fix: improvements all around

* add our own fan controller, will need more tuning with time, but it's a great start
* add restart/shutdown hooks for ulcmd, so the display shows the state of the system
* change how we expose unifi's libs to binaries
* Fixup systemd hang at boot due to networking
* move ubnthal to systemd task, since we don't load modules due to the unifi initramfs in the prebuilt kernel
This commit is contained in:
Chris Blake 2024-05-22 11:42:34 -05:00
parent 459d0a4758
commit a4f7f862c2
14 changed files with 235 additions and 27 deletions

View file

@ -15,7 +15,7 @@ datasource:
- omv-salt deploy run hosts
- usermod -a -G _ssh debian
# Setup network for both nics
# Setup network for both nics, needed so SFP+ works
network:
version: 2
ethernets:

View file

@ -0,0 +1 @@
/usr/lib/ubnt-fw

View file

@ -1,7 +0,0 @@
# /etc/modules: kernel modules to load at boot time.
#
# This file contains the names of kernel modules that should be loaded
# at boot time, one per line. Lines beginning with "#" are ignored.
# Parameters can be specified after the module name.
ubnthal # used by ulcmd

View file

@ -0,0 +1,11 @@
[Unit]
Description=Load UBNT kernel modules
[Service]
User=root
Type=oneshot
ExecStart=/usr/sbin/modprobe ubnthal
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,3 @@
[Service]
ExecStart=
ExecStart=/lib/systemd/systemd-networkd-wait-online --any

View file

@ -0,0 +1,11 @@
[Unit]
Description=ulcmd reboot hook
DefaultDependencies=no
Before=reboot.target
[Service]
ExecStart=/usr/bin/ulcmd --sender system-hook --command restart
Type=oneshot
[Install]
WantedBy=reboot.target

View file

@ -0,0 +1,11 @@
[Unit]
Description=ulcmd shutdown hook
DefaultDependencies=no
Before=shutdown.target halt.target
[Service]
ExecStart=/usr/bin/ulcmd --sender system-hook --command poweroff
Type=oneshot
[Install]
WantedBy=shutdown.target halt.target

View file

@ -0,0 +1,15 @@
[Unit]
Description=Daemon for MCU based LCM control
Requires=load-ubnt-modules.service
Requires=mock-ubnt-api.service
[Service]
Type=simple
ExecStart=/usr/bin/ulcmd
KillMode=process
Restart=on-failure
RestartSec=2s
TimeoutStopSec=2s
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,14 @@
[Unit]
Description=Fan Controller daemon for the UNVR Pro
Requires=load-ubnt-modules.service
[Service]
Type=simple
ExecStart=/usr/bin/unvr-fan-daemon
KillMode=process
Restart=on-failure
RestartSec=2s
TimeoutStopSec=2s
[Install]
WantedBy=multi-user.target

View file

@ -4,7 +4,8 @@ import socket
app = Flask(__name__)
@app.route('/api/info')
@app.route("/api/info")
def api_info():
print(socket.gethostname())
payload = {
@ -13,11 +14,12 @@ def api_info():
}
return jsonify(payload)
# No controllers for you
@app.route('/api/controllers')
def api_controllers():
payload = {}
return jsonify(payload)
if __name__ == '__main__':
# No controllers for you
@app.route("/api/controllers")
def api_controllers():
return jsonify({})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=11081)

View file

@ -0,0 +1,146 @@
#!/usr/bin/python3
from functools import lru_cache
import os
import re
import time
UBNTHAL_PATH = "/proc/ubnthal/system.info"
SMARTCTL_PATH = "/usr/sbin/smartctl"
THERMAL_SYS_PATHS = {
"UNVRPRO": {
"thermal": [
"/sys/devices/virtual/thermal/thermal_zone0/temp",
"/sys/class/hwmon/hwmon0/device/temp1_input",
"/sys/class/hwmon/hwmon0/device/temp2_input",
"/sys/class/hwmon/hwmon0/device/temp3_input",
],
"fan_pwms": [
"/sys/class/hwmon/hwmon0/device/pwm1",
"/sys/class/hwmon/hwmon0/device/pwm2",
"/sys/class/hwmon/hwmon0/device/pwm3",
],
},
}
@lru_cache(None)
def __get_ubnt_device():
try:
with open(UBNTHAL_PATH, "r") as f:
ubnthal_model = [i for i in f.readlines() if i.startswith("shortname=")][0]
return ubnthal_model.lstrip("shortname=").rstrip("\n")
except FileNotFoundError:
print(
f"Error: unable to open {UBNTHAL_PATH}; is the ubnthal kernel module loaded?!"
)
raise
def __get_board_temps():
# Are we supported?
if __get_ubnt_device() not in THERMAL_SYS_PATHS:
raise Exception(
f"Error: Your Unifi device of {__get_ubnt_device()} is not yet supported by unvr-fan-daemon! Exiting..."
)
# For each of our paths, get the temps, and append to single return list
board_temps = []
for path in THERMAL_SYS_PATHS[__get_ubnt_device()]["thermal"]:
try:
with open(path, "r") as f:
board_temps.append(int(f.readline().rstrip("\n")))
except FileNotFoundError:
print(f"Warning: Unable to open {path}; ignoring and continuing...")
continue
# Did we get ANY temps?!?
if len(board_temps) == 0:
raise Exception(
"Error: Unable to parse out any board temps for your device, something is really wrong! Exiting..."
)
return board_temps
def __get_disk_temps():
# Find the list of all devices, which could be none
devices = re.findall(
r"^[/a-z]+",
os.popen(f"{SMARTCTL_PATH} -n standby --scan").read(),
re.MULTILINE,
)
# For each disk, get the temp, and append to our list
disk_temps = []
for dev in devices:
dev_temp = re.search(
r"^194 [\w-]+\s+0x\d+\s+\d+\s+\d+\s+\d+\s+[\w-]+\s+\w+\s+\S+\s+(\d+)(?:\s[\(][^)]*[\)])?$",
os.popen(f"{SMARTCTL_PATH} -A {dev}").read(),
re.MULTILINE,
)
if dev_temp:
disk_temps.append(int(f"{dev_temp.group(1)}000")) # Append zeros
return disk_temps
def __calculate_fan_speed(temp):
# our basic fancurve logic
match temp:
case _ if temp < 40:
fanspeed = 25
case _ if temp >= 40 and temp < 50:
fanspeed = 75
case _ if temp >= 50 and temp < 60:
fanspeed = 150
case _ if temp >= 60 and temp < 70:
fanspeed = 200
case _:
fanspeed = 255
return fanspeed
def __set_fan_speed(speed: int):
# Set the fans
for fan in THERMAL_SYS_PATHS[__get_ubnt_device()]["fan_pwms"]:
try:
with open(fan, "w") as f:
f.write(str(speed))
except FileNotFoundError:
print(
f"Error: Unable to write to PWM at {path}! Why can't we set fan speed!?"
)
raise
if __name__ == "__main__":
# Trigger our model load so it's cached
__get_ubnt_device()
# Cache so we only write to PWMs if this changes
last_fanspeed = 0
print("unvr-fan-daemon starting...")
# Start with debug write to max speed so we hear it :)
__set_fan_speed(255)
time.sleep(1)
# Start our main loop
while True:
# Get the fanspeed we wanna set based on temps
temp = (
sorted(__get_board_temps() + __get_disk_temps(), reverse=True)[0] // 1000
) # Move temp to C, ignore decimals
fanspeed = __calculate_fan_speed(temp)
# If there's a change in calculated fan speed, set it
if last_fanspeed != fanspeed:
print(f"Setting fan PWMs to {fanspeed} due to temp of {temp}C")
__set_fan_speed(fanspeed)
last_fanspeed = fanspeed
# Sleep and run again
time.sleep(5)