PoE HAT 23W software missing

Hi,

I just bought the PoE HAT and tried to get it up and running. The following Wiki page is the guide I found: https://wiki.radxa.com/ROCKPI_23W_PoE_HAT

Unfortunately the domain cos.setq.me is not connectable and therefore I can’t get the FAN up and running. Is there an alternative source?

Hi Eckis,

We moved it to github, which is here: https://github.com/radxa/23w-poe

Object storage (COS) has anti-theft chain settings, browser access may be blocked, but command line access is normal. But to make it easier to access, we’ve moved it to github.

If you have any other questions, please let me know.

Hi,

Thanks a lot for the new location. I wasn’t successful in installing.

I’m puzzled, that the only OS supported by the script is Debian 9. I have no idea where to get that. On your https://wiki.radxa.com/Rock4/downloads#rock-4b+ is Debian 10 with Debian 11 as beta and on https://radxa.com/products/rock4/4bp#downloads is Debian 11 available.

I just have Debian 11 installed.

Your script suggests to install libmraa and rockpi4-dtbo. The newest edition of libmraa is libmraa2 and rockpi4-dtbo I couldn’t find in any repository.

What should I do to get your device running?

Hi,

I bought this product with the expectation that I get a usable product. Nothing was written that it is legacy and that it is not anymore manufactured.

Maybe I should write in the shopping website a resume telling, that the software is old and not anymore usable.

Your complaint is a common refrain on here, unfortunately. Some queries like yours will get an actual response, with varying degrees of usefulness. Others will just languish forever, getting no response at all.

This isn’t to say that there are no people on here who can help; I believe there are many very knowledgeable folks, but they’re just not of the sort who are going to go out of their way to assist. Then there are the people I think of as the “Radxa apologists”, who will (unhelpfully) tell you that if you want to fix your problem, you need to be running a supported distribution…neglecting the fact that most or all of those are literally years out of date.

I’ve seen the Radxa folks themselves explain that it’s more profitable to continually release new hardware than it is to make sure that the associated software stays fresh, especially if makers of similar hardware are just going to “steal” the fruits of their labor anyway. I would argue that this is how the world of Open Source works, but I do understand how that would be frustrating. Unfortunately, the end result is that the people who bought these devices in quantity for some sort of project fare just fine, while the folks like us who bought one because it was what we thought was the best option end up disappointed.

All that said, there are libraries available for controlling a fan via PWM, if you wanted to try rolling your own solution. I’m facing a similar conundrum, since I’ve been able to get basic on/off functionality to work fine, while variable speeds via PWM have so far eluded me.

I’m sorry that you ended up in a similar place as quite a number of others on here. I know exactly how disappointing that can be.

I’m using Ubuntu 22.04 on Rockpi 4A+ Ver1.72, but


didn’t work for me, so I created my own solution.

I’m enabling PWM0 and PWM1 with rsetup.

I’m sharing this here since I believe others might be facing the same issue.
Please make any modifications yourself as needed.

#!/usr/bin/env python3
import os
import time
import glob
from configparser import ConfigParser

# Constants
GPIO_FAN = 154  # GPIO154 for fan ON/OFF control
PERIOD = 1000000  # 1ms (1kHz)
CONFIG_PATH = "/etc/rockpi-poe.conf"
PWM1_DEVICE = "ff420010.pwm"  # Device corresponding to PWM1

def get_pwm_chip():
    """Automatically get the pwmchipX corresponding to PWM1 (ff420010.pwm)"""
    pwm_chips = glob.glob("/sys/class/pwm/pwmchip*")
    for chip in pwm_chips:
        device_path = os.path.realpath(f"{chip}/device")
        if PWM1_DEVICE in device_path:
            return os.path.basename(chip)  # Get pwmchipX
    return None

# Dynamically get pwmchipX
PWM_CHIP = get_pwm_chip()
PWM_CHANNEL = "pwm0"
PWM_PATH = f"/sys/class/pwm/{PWM_CHIP}/{PWM_CHANNEL}/"

if PWM_CHIP is None:
    raise RuntimeError(f"PWM1 ({PWM1_DEVICE}) pwmchipX not found!")

def export_gpio():
    """Export GPIO154 (only on first execution)"""
    if not os.path.exists(f"/sys/class/gpio/gpio{GPIO_FAN}"):
        with open("/sys/class/gpio/export", "w") as f:
            f.write(str(GPIO_FAN))
        time.sleep(1)  # Wait for reflection

def set_gpio(value):
    """Control ON/OFF of GPIO154"""
    with open(f"/sys/class/gpio/gpio{GPIO_FAN}/direction", "w") as f:
        f.write("out")
    with open(f"/sys/class/gpio/gpio{GPIO_FAN}/value", "w") as f:
        f.write(str(value))

def enable_pwm():
    """Enable PWM"""
    if not os.path.exists(PWM_PATH):
        with open(f"/sys/class/pwm/{PWM_CHIP}/export", "w") as f:
            f.write("0")
        time.sleep(2)  # Wait for reflection

        # Wait until the pwm0 directory is created
        timeout = 5
        while not os.path.exists(PWM_PATH) and timeout > 0:
            time.sleep(0.5)
            timeout -= 1

        if not os.path.exists(PWM_PATH):
            raise RuntimeError(f"PWM0 directory was not created: {PWM_PATH}")

    # Apply settings
    write_pwm("period", PERIOD)
    write_pwm("enable", 1)
    write_pwm("polarity", "normal")

def write_pwm(filename, value):
    """Write PWM settings"""
    with open(f"{PWM_PATH}{filename}", "w") as f:
        f.write(str(value))

def read_sensor_temp():
    """Get temperature from PoE HAT sensor"""
    try:
        v2t = lambda x: 42 + (960 - x) * 0.05  # Temperature conversion formula
        with open('/sys/bus/iio/devices/iio:device0/in_voltage0_raw') as f:
            raw_value = f.read().strip()
            if not raw_value.isdigit():  # If not a number, return an invalid value
                return 0
            return v2t(int(raw_value))
    except Exception:
        return 0  # Return 0 if unable to retrieve

def read_soc_temp(zone=0):
    """Get SoC (CPU/GPU) temperature"""
    try:
        with open(f"/sys/class/thermal/thermal_zone{zone}/temp") as f:
            return int(f.read().strip()) / 1000.0  # Convert from milli-degrees Celsius to Celsius
    except Exception:
        return 0  # Return 0 if unable to retrieve

def read_temp():
    """Get the highest temperature among PoE HAT, CPU, and GPU"""
    return max(read_sensor_temp(), read_soc_temp(0), read_soc_temp(1))

def read_conf():
    """Read temperature thresholds from the configuration file"""
    conf = {"lv0": 40, "lv1": 45, "lv2": 50, "lv3": 55}
    try:
        cfg = ConfigParser()
        cfg.read(CONFIG_PATH)
        conf["lv0"] = cfg.getint("fan", "lv0", fallback=40)
        conf["lv1"] = cfg.getint("fan", "lv1", fallback=45)
        conf["lv2"] = cfg.getint("fan", "lv2", fallback=50)
        conf["lv3"] = cfg.getint("fan", "lv3", fallback=55)
    except Exception:
        pass
    return conf

def set_fan_speed():
    """Adjust fan speed according to temperature"""
    conf = read_conf()
    while True:
        temp = read_temp()

        # Default to minimum rotation (25%)
        duty_cycle = 0.75 * PERIOD
        level = "25%"  # Initial value

        if temp == 0:
            print("Temperature sensor data unavailable!")
            level = "25%"  # Set to minimum rotation for safety
        elif temp >= conf["lv3"]:
            duty_cycle = PERIOD
            level = "100%"
        elif temp >= conf["lv2"]:
            duty_cycle = 0.75 * PERIOD
            level = "75%"
        elif temp >= conf["lv1"]:
            duty_cycle = 0.50 * PERIOD
            level = "50%"
        elif temp >= conf["lv0"]:
            duty_cycle = 0.25 * PERIOD
            level = "25%"
        else:
            duty_cycle = 0
            level = "Stopped"

        # Output temperature + fan speed level only
        print(f"{temp}°C → Fan speed: {level}")

        write_pwm("duty_cycle", int(duty_cycle))
        time.sleep(10)

def start():
    """Start the fan"""
    export_gpio()
    set_gpio(1)  # Turn GPIO154 ON
    enable_pwm()
    set_fan_speed()

def stop():
    """Stop the fan"""
    set_gpio(0)  # Turn GPIO154 OFF
    if os.path.exists(PWM_PATH):
        write_pwm("enable", 0)

if __name__ == "__main__":
    import sys
    if len(sys.argv) < 2:
        print("Usage: python3 rockpi-poe.py start|stop")
        sys.exit(1)

    if sys.argv[1] == "start":
        start()
    elif sys.argv[1] == "stop":
        stop()
    else:
        print("Usage: python3 rockpi-poe.py start|stop")