diff --git a/README.md b/README.md index cf73a60..7c7b72d 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/overlay/filesystem/etc/cloud/cloud.cfg.d/99-none-omv-setup.cfg b/overlay/filesystem/etc/cloud/cloud.cfg.d/99-none-omv-setup.cfg index f100942..40aacc6 100644 --- a/overlay/filesystem/etc/cloud/cloud.cfg.d/99-none-omv-setup.cfg +++ b/overlay/filesystem/etc/cloud/cloud.cfg.d/99-none-omv-setup.cfg @@ -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: diff --git a/overlay/filesystem/etc/ld.so.conf.d/ubnt.conf b/overlay/filesystem/etc/ld.so.conf.d/ubnt.conf new file mode 100644 index 0000000..509c20b --- /dev/null +++ b/overlay/filesystem/etc/ld.so.conf.d/ubnt.conf @@ -0,0 +1 @@ +/usr/lib/ubnt-fw diff --git a/overlay/filesystem/etc/modules b/overlay/filesystem/etc/modules deleted file mode 100644 index a6c9853..0000000 --- a/overlay/filesystem/etc/modules +++ /dev/null @@ -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 diff --git a/overlay/filesystem/etc/systemd/system/load-ubnt-modules.service b/overlay/filesystem/etc/systemd/system/load-ubnt-modules.service new file mode 100644 index 0000000..d3d0d16 --- /dev/null +++ b/overlay/filesystem/etc/systemd/system/load-ubnt-modules.service @@ -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 diff --git a/overlay/filesystem/etc/systemd/system/systemd-networkd-wait-online.service.d/override.conf b/overlay/filesystem/etc/systemd/system/systemd-networkd-wait-online.service.d/override.conf new file mode 100644 index 0000000..1ef5a03 --- /dev/null +++ b/overlay/filesystem/etc/systemd/system/systemd-networkd-wait-online.service.d/override.conf @@ -0,0 +1,3 @@ +[Service] +ExecStart= +ExecStart=/lib/systemd/systemd-networkd-wait-online --any diff --git a/overlay/filesystem/etc/systemd/system/ulcmd-reboot-hook.service b/overlay/filesystem/etc/systemd/system/ulcmd-reboot-hook.service new file mode 100644 index 0000000..998d1b3 --- /dev/null +++ b/overlay/filesystem/etc/systemd/system/ulcmd-reboot-hook.service @@ -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 diff --git a/overlay/filesystem/etc/systemd/system/ulcmd-shutdown-hook.service b/overlay/filesystem/etc/systemd/system/ulcmd-shutdown-hook.service new file mode 100644 index 0000000..d439387 --- /dev/null +++ b/overlay/filesystem/etc/systemd/system/ulcmd-shutdown-hook.service @@ -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 diff --git a/overlay/filesystem/etc/systemd/system/ulcmd.service b/overlay/filesystem/etc/systemd/system/ulcmd.service new file mode 100644 index 0000000..c17b036 --- /dev/null +++ b/overlay/filesystem/etc/systemd/system/ulcmd.service @@ -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 diff --git a/overlay/filesystem/etc/systemd/system/unvr-fan-daemon.service b/overlay/filesystem/etc/systemd/system/unvr-fan-daemon.service new file mode 100644 index 0000000..f098885 --- /dev/null +++ b/overlay/filesystem/etc/systemd/system/unvr-fan-daemon.service @@ -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 diff --git a/overlay/filesystem/usr/bin/mock-ubnt-api b/overlay/filesystem/usr/bin/mock-ubnt-api index c3eece6..548a0b1 100755 --- a/overlay/filesystem/usr/bin/mock-ubnt-api +++ b/overlay/filesystem/usr/bin/mock-ubnt-api @@ -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) diff --git a/overlay/filesystem/usr/bin/unvr-fan-daemon b/overlay/filesystem/usr/bin/unvr-fan-daemon new file mode 100755 index 0000000..a50fc9e --- /dev/null +++ b/overlay/filesystem/usr/bin/unvr-fan-daemon @@ -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) diff --git a/scripts/docker/bootstrap/001-bootstrap b/scripts/docker/bootstrap/001-bootstrap index 78eebfa..cc61eb7 100755 --- a/scripts/docker/bootstrap/001-bootstrap +++ b/scripts/docker/bootstrap/001-bootstrap @@ -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 \ diff --git a/scripts/docker/run_debootstrap.sh b/scripts/docker/run_debootstrap.sh index c4451d3..96b96e4 100755 --- a/scripts/docker/run_debootstrap.sh +++ b/scripts/docker/run_debootstrap.sh @@ -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