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

@ -8,7 +8,7 @@ Firmware builder to convert your Unifi NVR Pro into an OpenMediaVault NAS applia
* UNVR Pro
Note that the 1U UNVR is not currently supported!
Note that the 1U UNVR is not currently supported as I do not have a unit to test on. PR's are welcome!
## Usage
@ -25,9 +25,9 @@ Note that the 1U UNVR is not currently supported!
## Installation
Note that currently the install process requires UART to modify the u-boot env for booting.
Note that currently the install process requires UART to modify the u-boot env for booting. In the future, if I can get the latest kernel GPL source, this will not be required.
1. Make sure your UNVR is running the same Unifi firmware as referenced in the README.md in the unifi-firmware directory.
1. Make sure your UNVR Pro is running the same Unifi firmware as referenced in the README.md in the unifi-firmware directory.
1. Build the firmware image (follow the Usage section), and then throw it on an HDD/SSD formatted to ext4. Put said HDD in the UNVR Pro as the only hard drive.
2. Hook up UART to the UNVR Pro (4 pin header on the PCB near the DC Power Backup port).
3. Boot the UNVR Pro, and press Escape twice when prompted to get to the u-boot shell. You only have 2 seconds to do this!
@ -58,7 +58,7 @@ Note that currently the install process requires UART to modify the u-boot env f
reboot
```
8. At this point you can remove the HDD/SSD you used, and enjoy Debian 12 with OpenMediaVault on your UNVR Pro! Default login for OpenMediaVault is `admin:openmediavault`. SSH login information is `debian:debian`.
8. At this point you can remove the HDD/SSD you used, and enjoy Debian 12 with OpenMediaVault on your UNVR Pro! Default login for OpenMediaVault is `admin:openmediavault`. SSH login information is `debian:debian`. Please note that first boot may take a bit as cloud-init runs to finish the setup.
## Removal
@ -87,15 +87,14 @@ To restore back to the factory UNVR-Pro firmware, you can do the following steps
* Installation is Hard
* Need to simplify the install process, this should be much easier once I can get latest GPL kernel source (no more uboot env stuff)
* Touchscreen
* disks do not populate atm, cuz no grpc service mocking/replacing ustated
* Disks do not populate ATM, because there is no grpc service mocking/replacing ustated, which ulcmd uses to get disk info.
* OpenMediaVault
* BTRFS does not work, period
* No kernel module in UBNT kernel, need new kernel source and we can make so many things better...
* Fans
* No service monitoring temps to adjust fan speed, so fans just stay at low spin from u-boot. Fans are on an i2c adt7475 controller
* Might try to build an out-of-tree module for this, more research needed
* Reset Button
* Does literally nothing ATM, not sure if it's worth having it do something or not
## Disclaimer
Note that since prebuild Ubiquiti software is required for this tool to work, this repo will never have prebuilt images available. This is to prevent redistribution of Ubiquiti's IP, so please do not ask! Also, by using this repo you accept all risk associated with it including but not limited to voiding your warranty and releasing all parties from any liability associated with your device and this software.
Note that since prebuild Ubiquiti software is required for this tool to work, this repo will never have prebuilt images available. This is to prevent redistribution of Ubiquiti's IP, so please DO NOT ASK! Also, by using this repo you accept all risk associated with it including but not limited to voiding your warranty and releasing all parties from any liability associated with your device and this software.

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)

View file

@ -15,9 +15,13 @@ apt-get clean
apt-get update
apt-mark hold linux-image-* # We do not want these, as we run our own kernel!
# Setup ulcmd
systemctl enable ulcmd
# Setup our services
systemctl enable load-ubnt-modules
systemctl enable mock-ubnt-api
systemctl enable ulcmd
systemctl enable ulcmd-reboot-hook
systemctl enable ulcmd-shutdown-hook
systemctl enable unvr-fan-daemon
# Now that we have our wanted kernel in place, do the rest of our installs
apt-get -o Dpkg::Options::="--force-confold" -y --allow-downgrades \

View file

@ -43,7 +43,6 @@ console-common console-data/keymap/full select us
" > ${build_path}/rootfs/debconf.set
# Copy over stuff for ulcmd, this is hacky, but that's this ENTIRE repo for you
mv "${build_path}/fw-extract/rootfs/lib/systemd/system/ulcmd.service" "${build_path}/rootfs/lib/systemd/system/ulcmd.service"
mv "${build_path}/fw-extract/rootfs/usr/bin/ulcmd" "${build_path}/rootfs/usr/bin/ulcmd" # LCD controller
mv "${build_path}/fw-extract/rootfs/usr/share/firmware" "${build_path}/rootfs/usr/share/" # LCD panel firmwares
mkdir -p "${build_path}/rootfs/usr/lib/ubnt-fw/" # Home for ulcmd libraries
@ -51,7 +50,6 @@ for file in libgrpc++.so.1 libgrpc.so.10 libprotobuf.so.23 \
libssl.so.1.1 libcrypto.so.1.1 libabsl*.so.20200923 libatomic.so.1; do
cp -H ${build_path}/fw-extract/rootfs/usr/lib/aarch64-linux-gnu/${file} "${build_path}/rootfs/usr/lib/ubnt-fw/"
done
sed -i 's|Type=simple|Type=simple\nEnvironment="LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/lib/ubnt-fw"|g' "${build_path}/rootfs/lib/systemd/system/ulcmd.service" # Add library path
# Kick off bash setup script within chroot
cp ${docker_scripts_path}/bootstrap/001-bootstrap ${build_path}/rootfs/bootstrap