mirror of
https://github.com/compute-blade-community/compute-blade-agent.git
synced 2026-04-16 15:35:42 +02:00
feat(hal): add RK3588 (Radxa CM5) HAL with sysfs fan control (#155)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Cedric Specht <cedric@specht-labs.de>
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -186,4 +186,8 @@ Temporary Items
|
|||||||
.history
|
.history
|
||||||
.ionide
|
.ionide
|
||||||
|
|
||||||
# End of https://www.toptal.com/developers/gitignore/api/go,visualstudiocode,goland+all,macos,linux
|
# End of https://www.toptal.com/developers/gitignore/api/go,visualstudiocode,goland+all,macos,linux
|
||||||
|
# Build artifacts
|
||||||
|
agent
|
||||||
|
bladectl
|
||||||
|
compute-blade-agent
|
||||||
|
|||||||
178
pkg/hal/hal_rk3588.go
Normal file
178
pkg/hal/hal_rk3588.go
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
//go:build linux && !tinygo
|
||||||
|
|
||||||
|
package hal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/compute-blade-community/compute-blade-agent/pkg/hal/led"
|
||||||
|
"github.com/compute-blade-community/compute-blade-agent/pkg/log"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
rk3588ThermalZonePath = "/sys/class/thermal/thermal_zone0/temp"
|
||||||
|
rk3588PwmFanHwmonName = "pwmfan"
|
||||||
|
)
|
||||||
|
|
||||||
|
// rk3588 implements the ComputeBladeHal interface for the Rockchip RK3588 (Radxa CM5).
|
||||||
|
// Fan control uses the kernel's pwmfan driver via sysfs. GPIO-dependent features
|
||||||
|
// (button, stealth hardware, PoE detection, LEDs, tachometer) are stubbed until the
|
||||||
|
// Rockchip-to-B2B-connector pin mapping is determined.
|
||||||
|
type rk3588 struct {
|
||||||
|
opts ComputeBladeHalOpts
|
||||||
|
pwmPath string // e.g. /sys/class/hwmon/hwmon8/pwm1
|
||||||
|
pwmEnablePath string // e.g. /sys/class/hwmon/hwmon8/pwm1_enable
|
||||||
|
stealthMode bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile-time interface check
|
||||||
|
var _ ComputeBladeHal = &rk3588{}
|
||||||
|
|
||||||
|
func newRk3588Hal(ctx context.Context, opts ComputeBladeHalOpts) (*rk3588, error) {
|
||||||
|
logger := log.FromContext(ctx)
|
||||||
|
|
||||||
|
pwmPath, err := findHwmonPwm(rk3588PwmFanHwmonName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to find pwmfan hwmon device: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
enablePath := pwmPath + "_enable"
|
||||||
|
|
||||||
|
// Set manual control mode (1 = manual PWM control)
|
||||||
|
if err := os.WriteFile(enablePath, []byte("1"), 0644); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to set pwm1_enable to manual mode: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
computeModule.WithLabelValues("radxa-cm5").Set(1)
|
||||||
|
|
||||||
|
logger.Info("starting hal setup", zap.String("hal", "rk3588"))
|
||||||
|
logger.Warn("GPIO pin mapping unknown for RK3588 B2B connector — button, stealth hardware, PoE detection, LEDs, and tachometer are stubbed")
|
||||||
|
|
||||||
|
return &rk3588{
|
||||||
|
opts: opts,
|
||||||
|
pwmPath: pwmPath,
|
||||||
|
pwmEnablePath: enablePath,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rk *rk3588) Run(ctx context.Context) error {
|
||||||
|
fanUnit.WithLabelValues("sysfs").Set(1)
|
||||||
|
<-ctx.Done()
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rk *rk3588) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFanSpeed sets the fan speed via sysfs pwm1 (0-100% mapped to 0-255).
|
||||||
|
func (rk *rk3588) SetFanSpeed(speed uint8) error {
|
||||||
|
fanTargetPercent.Set(float64(speed))
|
||||||
|
|
||||||
|
var pwmVal uint8
|
||||||
|
if speed == 0 {
|
||||||
|
pwmVal = 0
|
||||||
|
} else if speed >= 100 {
|
||||||
|
pwmVal = 255
|
||||||
|
} else {
|
||||||
|
pwmVal = uint8(float64(speed) * 255.0 / 100.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.WriteFile(rk.pwmPath, []byte(strconv.Itoa(int(pwmVal))), 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFanRPM returns 0 — no tachometer GPIO is mapped on the RK3588.
|
||||||
|
func (rk *rk3588) GetFanRPM() (float64, error) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTemperature returns the SoC temperature in degrees Celsius.
|
||||||
|
func (rk *rk3588) GetTemperature() (float64, error) {
|
||||||
|
f, err := os.Open(rk3588ThermalZonePath)
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
defer func() { _ = f.Close() }()
|
||||||
|
|
||||||
|
raw, err := io.ReadAll(f)
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cpuTemp, err := strconv.Atoi(strings.TrimSpace(string(raw)))
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
temp := float64(cpuTemp) / 1000.0
|
||||||
|
socTemperature.Set(temp)
|
||||||
|
|
||||||
|
return temp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetStealthMode tracks stealth mode in software only (no GPIO mapped).
|
||||||
|
func (rk *rk3588) SetStealthMode(enabled bool) error {
|
||||||
|
rk.stealthMode = enabled
|
||||||
|
if enabled {
|
||||||
|
stealthModeEnabled.Set(1)
|
||||||
|
} else {
|
||||||
|
stealthModeEnabled.Set(0)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rk *rk3588) StealthModeActive() bool {
|
||||||
|
return rk.stealthMode
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLed is a no-op — WS281x LED GPIO pin mapping is unknown on RK3588.
|
||||||
|
func (rk *rk3588) SetLed(idx LedIndex, color led.Color) error {
|
||||||
|
ledColorChangeEventCount.Inc()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPowerStatus returns PowerPoeOrUsbC as a safe default (no PoE detection GPIO mapped).
|
||||||
|
func (rk *rk3588) GetPowerStatus() (PowerStatus, error) {
|
||||||
|
powerStatus.WithLabelValues(fmt.Sprint(PowerPoe802at)).Set(0)
|
||||||
|
powerStatus.WithLabelValues(fmt.Sprint(PowerPoeOrUsbC)).Set(1)
|
||||||
|
return PowerPoeOrUsbC, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WaitForEdgeButtonPress blocks until context cancellation (no button GPIO mapped).
|
||||||
|
func (rk *rk3588) WaitForEdgeButtonPress(ctx context.Context) error {
|
||||||
|
<-ctx.Done()
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// findHwmonPwm scans /sys/class/hwmon/hwmon*/name for a device matching the given name
|
||||||
|
// and returns the path to its pwm1 file.
|
||||||
|
func findHwmonPwm(name string) (string, error) {
|
||||||
|
matches, err := filepath.Glob("/sys/class/hwmon/hwmon*/name")
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to glob hwmon devices: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, namePath := range matches {
|
||||||
|
raw, err := os.ReadFile(namePath)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(string(raw)) == name {
|
||||||
|
dir := filepath.Dir(namePath)
|
||||||
|
pwmPath := filepath.Join(dir, "pwm1")
|
||||||
|
if _, err := os.Stat(pwmPath); err != nil {
|
||||||
|
return "", fmt.Errorf("found %s hwmon at %s but pwm1 does not exist: %w", name, dir, err)
|
||||||
|
}
|
||||||
|
return pwmPath, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("no hwmon device found with name %q", name)
|
||||||
|
}
|
||||||
@@ -31,6 +31,8 @@ func NewHal(ctx context.Context, opts ComputeBladeHalOpts) (ComputeBladeHal, err
|
|||||||
return newBcm2712Hal(ctx, opts)
|
return newBcm2712Hal(ctx, opts)
|
||||||
case strings.Contains(compatStr, "bcm2711"):
|
case strings.Contains(compatStr, "bcm2711"):
|
||||||
return newBcm2711Hal(ctx, opts)
|
return newBcm2711Hal(ctx, opts)
|
||||||
|
case strings.Contains(compatStr, "rockchip,rk3588"):
|
||||||
|
return newRk3588Hal(ctx, opts)
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported platform: %s", strings.ReplaceAll(compatStr, "\x00", ", "))
|
return nil, fmt.Errorf("unsupported platform: %s", strings.ReplaceAll(compatStr, "\x00", ", "))
|
||||||
}
|
}
|
||||||
|
|||||||
115
scripts/find-tach-gpio.sh
Executable file
115
scripts/find-tach-gpio.sh
Executable file
@@ -0,0 +1,115 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# find-tach-gpio.sh - Safely probe GPIOs to find fan tachometer signal
|
||||||
|
# Run on Radxa node with fan at 100%: sudo ./find-tach-gpio.sh
|
||||||
|
#
|
||||||
|
# The tachometer generates 2 pulses per revolution. At 5000 RPM:
|
||||||
|
# 5000 RPM / 60 = 83.33 RPS * 2 pulses = ~167 Hz
|
||||||
|
# At 3000 RPM: ~100 Hz
|
||||||
|
# We look for any GPIO showing periodic edge events.
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
PROBE_DURATION=1 # seconds to monitor each GPIO
|
||||||
|
MIN_EVENTS=10 # minimum events to consider "active" (10 events in 1s = 600 RPM minimum)
|
||||||
|
DELAY_BETWEEN=0.5 # seconds between probes to let system settle
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
echo "=== Fan Tachometer GPIO Finder ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check if running as root (needed for gpiomon)
|
||||||
|
if [ "$EUID" -ne 0 ]; then
|
||||||
|
echo -e "${RED}Please run as root (sudo)${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check fan speed
|
||||||
|
FAN_PWM=$(cat /sys/class/hwmon/hwmon8/pwm1 2>/dev/null || echo "unknown")
|
||||||
|
echo "Current fan PWM: $FAN_PWM (should be 255 for best detection)"
|
||||||
|
if [ "$FAN_PWM" != "255" ]; then
|
||||||
|
echo -e "${YELLOW}Setting fan to 100% for detection...${NC}"
|
||||||
|
echo 255 > /sys/class/hwmon/hwmon8/pwm1
|
||||||
|
sleep 2 # Let fan spin up
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Get list of gpiochips
|
||||||
|
CHIPS=$(ls /dev/gpiochip* | sed 's|/dev/||')
|
||||||
|
|
||||||
|
echo "Scanning GPIO chips: $CHIPS"
|
||||||
|
echo "Probe duration: ${PROBE_DURATION}s per line, minimum ${MIN_EVENTS} events to flag"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
FOUND_CANDIDATES=""
|
||||||
|
|
||||||
|
for chip in $CHIPS; do
|
||||||
|
# Get number of lines for this chip
|
||||||
|
NUM_LINES=$(gpioinfo $chip 2>/dev/null | wc -l)
|
||||||
|
NUM_LINES=$((NUM_LINES - 1)) # Subtract header line
|
||||||
|
|
||||||
|
if [ "$NUM_LINES" -le 0 ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${YELLOW}=== Scanning $chip ($NUM_LINES lines) ===${NC}"
|
||||||
|
|
||||||
|
for line in $(seq 0 $((NUM_LINES - 1))); do
|
||||||
|
# Check if line is already in use
|
||||||
|
LINE_INFO=$(gpioinfo $chip 2>/dev/null | grep "line *$line:" || true)
|
||||||
|
if echo "$LINE_INFO" | grep -q "\[used\]"; then
|
||||||
|
# Skip lines that are in use
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Probe the line
|
||||||
|
printf " Line %2d: " "$line"
|
||||||
|
|
||||||
|
# Run gpiomon with timeout, count events
|
||||||
|
EVENTS=$(timeout ${PROBE_DURATION}s gpiomon --num-events=100 $chip $line 2>&1 | wc -l || echo "0")
|
||||||
|
|
||||||
|
if [ "$EVENTS" -ge "$MIN_EVENTS" ]; then
|
||||||
|
# Calculate approximate frequency
|
||||||
|
FREQ=$((EVENTS / PROBE_DURATION))
|
||||||
|
RPM_ESTIMATE=$((FREQ * 60 / 2)) # 2 pulses per revolution
|
||||||
|
echo -e "${GREEN}ACTIVE! $EVENTS events (~${FREQ} Hz, ~${RPM_ESTIMATE} RPM)${NC}"
|
||||||
|
FOUND_CANDIDATES="$FOUND_CANDIDATES\n $chip line $line: $EVENTS events (~${RPM_ESTIMATE} RPM)"
|
||||||
|
elif [ "$EVENTS" -gt 0 ]; then
|
||||||
|
echo "$EVENTS events (noise?)"
|
||||||
|
else
|
||||||
|
echo "no events"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Small delay to let system settle
|
||||||
|
sleep $DELAY_BETWEEN
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "=== Scan Complete ==="
|
||||||
|
if [ -n "$FOUND_CANDIDATES" ]; then
|
||||||
|
echo -e "${GREEN}Candidate tachometer GPIOs found:${NC}"
|
||||||
|
echo -e "$FOUND_CANDIDATES"
|
||||||
|
echo ""
|
||||||
|
echo "To verify, try monitoring the candidate with varying fan speeds:"
|
||||||
|
echo " gpiomon --num-events=50 <chip> <line>"
|
||||||
|
echo ""
|
||||||
|
echo "Then add to device tree or HAL configuration."
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}No active GPIOs found.${NC}"
|
||||||
|
echo "Possible reasons:"
|
||||||
|
echo " - Fan tachometer not connected on this carrier board"
|
||||||
|
echo " - Tachometer uses a different interface (I2C, ADC, etc.)"
|
||||||
|
echo " - Fan doesn't have tachometer wire connected"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Restore fan to auto if we changed it
|
||||||
|
if [ "$FAN_PWM" != "255" ] && [ "$FAN_PWM" != "unknown" ]; then
|
||||||
|
echo ""
|
||||||
|
echo "Restoring fan PWM to $FAN_PWM"
|
||||||
|
echo "$FAN_PWM" > /sys/class/hwmon/hwmon8/pwm1
|
||||||
|
fi
|
||||||
Reference in New Issue
Block a user