diff --git a/.gitignore b/.gitignore index 96b6487..9a300d0 100644 --- a/.gitignore +++ b/.gitignore @@ -186,4 +186,8 @@ Temporary Items .history .ionide -# End of https://www.toptal.com/developers/gitignore/api/go,visualstudiocode,goland+all,macos,linux \ No newline at end of file +# End of https://www.toptal.com/developers/gitignore/api/go,visualstudiocode,goland+all,macos,linux +# Build artifacts +agent +bladectl +compute-blade-agent diff --git a/pkg/hal/hal_rk3588.go b/pkg/hal/hal_rk3588.go new file mode 100644 index 0000000..5df77d1 --- /dev/null +++ b/pkg/hal/hal_rk3588.go @@ -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) +} diff --git a/pkg/hal/platform_linux.go b/pkg/hal/platform_linux.go index 4105ccf..4821ca1 100644 --- a/pkg/hal/platform_linux.go +++ b/pkg/hal/platform_linux.go @@ -31,6 +31,8 @@ func NewHal(ctx context.Context, opts ComputeBladeHalOpts) (ComputeBladeHal, err return newBcm2712Hal(ctx, opts) case strings.Contains(compatStr, "bcm2711"): return newBcm2711Hal(ctx, opts) + case strings.Contains(compatStr, "rockchip,rk3588"): + return newRk3588Hal(ctx, opts) default: return nil, fmt.Errorf("unsupported platform: %s", strings.ReplaceAll(compatStr, "\x00", ", ")) } diff --git a/scripts/find-tach-gpio.sh b/scripts/find-tach-gpio.sh new file mode 100755 index 0000000..8865138 --- /dev/null +++ b/scripts/find-tach-gpio.sh @@ -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 " + 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