Files
compute-blade-agent/pkg/hal/hal_bcm2711.go
T
Cedric Kienzler 781ded8e43 feat(bladectl)!: add more bladectl commands (#91)
This PR introduces a comprehensive set of new subcommands to bladectl, expanding its capabilities for querying and managing compute blade state. It also includes an internal refactor to simplify interface management across the gRPC API.

* `get`
	* `fan`: Returns current fan speed.
	* `identify`: Indicates whether the identify mode is active.
	* `stealth`: Shows if stealth mode is currently enabled.
	* `status`: Prints a full blade status report.
	* `temperature`: Retrieves current SoC temperature.
	* `critical`: Shows whether critical mode is active.
	* `power`: Reports the current power source (e.g., PoE+ or USB).
* `set`
	* `stealth`: Enables stealth mode.
* `remove`
	* `stealth`: Disables stealth mode.
* `describe`
	* `fan`: Outputs the current fan curve configuration.
* `monitor`: plot some charts about the state of the compute-blade-agent

* **gRPC API refactor**: The gRPC service definitions previously located in `internal/api` have been folded into `internal/agent`. This eliminates redundant interface declarations and ensures that all ComputeBladeAgent implementations are directly compatible with the gRPC API.
This reduces duplication and improves long-term maintainability and clarity of the interface contract.

```bash
bladectl set fan --percent 90 --blade 1 --blade 2
bladectl unset identify --blade 1 --blade 2 --blade 3 --blade 4
bladectl set stealth --blade 1 --blade 2 --blade 3 --blade 4
bladectl get status --blade 1 --blade 2 --blade 3 --blade 4
┌───────┬─────────────┬────────────────────┬───────────────┬──────────────┬──────────┬───────────────┬──────────────┐
│ BLADE │ TEMPERATURE │ FAN SPEED OVERRIDE │ FAN SPEED     │ STEALTH MODE │ IDENTIFY │ CRITICAL MODE │ POWER STATUS │
├───────┼─────────────┼────────────────────┼───────────────┼──────────────┼──────────┼───────────────┼──────────────┤
│ 1     │ 50°C        │ 90%                │ 5825 RPM(90%) │ Active       │ Off      │ Off           │ poe+         │
│ 2     │ 48°C        │ 90%                │ 5825 RPM(90%) │ Active       │ Off      │ Off           │ poe+         │
│ 3     │ 49°C        │ Not set            │ 4643 RPM(56%) │ Active       │ Off      │ Off           │ poe+         │
│ 4     │ 49°C        │ Not set            │ 4774 RPM(58%) │ Active       │ Off      │ Off           │ poe+         │
└───────┴─────────────┴────────────────────┴───────────────┴──────────────┴──────────┴───────────────┴──────────────┘
bladectl rm stealth --blade 1 --blade 2 --blade 3 --blade 4
bladectl rm fan --blade 1 --blade 2 --blade 3 --blade 4
bladectl get status --blade 1 --blade 2 --blade 3 --blade 4
┌───────┬─────────────┬────────────────────┬───────────────┬──────────────┬──────────┬───────────────┬──────────────┐
│ BLADE │ TEMPERATURE │ FAN SPEED OVERRIDE │ FAN SPEED     │ STEALTH MODE │ IDENTIFY │ CRITICAL MODE │ POWER STATUS │
├───────┼─────────────┼────────────────────┼───────────────┼──────────────┼──────────┼───────────────┼──────────────┤
│ 1     │ 51°C        │ Not set            │ 5177 RPM(66%) │ Off          │ Off      │ Off           │ poe+         │
│ 2     │ 49°C        │ Not set            │ 5177 RPM(58%) │ Off          │ Off      │ Off           │ poe+         │
│ 3     │ 50°C        │ Not set            │ 4659 RPM(60%) │ Off          │ Off      │ Off           │ poe+         │
│ 4     │ 48°C        │ Not set            │ 4659 RPM(54%) │ Off          │ Off      │ Off           │ poe+         │
└───────┴─────────────┴────────────────────┴───────────────┴──────────────┴──────────┴───────────────┴──────────────┘
```

when having multiple compute-blades in your bladeconfig:

```yaml
blades:
    - name: 1
      blade:
        server: blade-pi1:8081
        cert:
            certificate-authority-data: <redacted>
            client-certificate-data: <redacted>
            client-key-data: <redacted>
    - name: 2
      blade:
        server: blade-pi2:8081
        cert:
            certificate-authority-data: <redacted>
            client-certificate-data: <redacted>
            client-key-data: <redacted>
    - name: 3
      blade:
        server: blade-pi3:8081
        cert:
            certificate-authority-data: <redacted>
            client-certificate-data: <redacted>
            client-key-data: <redacted>
    - name: 4
      blade:
        server: blade-pi4:8081
        cert:
            certificate-authority-data: <redacted>
            client-certificate-data: <redacted>
            client-key-data: <redacted>
    - name: 4
      blade:
        server: blade-pi4:8081
        cert:
            certificate-authority-data: <redacted>
            client-certificate-data: <redacted>
            client-key-data: <redacted>
current-blade: 1
```

Fixes #4, #9 (partially), should help with #5

* test: improve unit-testing

* fix: pin github.com/warthog618/gpiod

---------

Co-authored-by: Cedric Kienzler <cedric@specht-labs.de>
2025-06-06 23:03:43 +02:00

510 lines
15 KiB
Go

//go:build linux && !tinygo
package hal
import (
"context"
"errors"
"fmt"
"io"
"os"
"strconv"
"strings"
"sync"
"syscall"
"time"
"github.com/compute-blade-community/compute-blade-agent/pkg/hal/led"
"github.com/compute-blade-community/compute-blade-agent/pkg/log"
"github.com/warthog618/gpiod"
"github.com/warthog618/gpiod/device/rpi"
"go.uber.org/zap"
"golang.org/x/sync/errgroup"
)
const (
bcm2711PeripheryBaseAddr = 0xFE000000
bcm2711RegPwmAddr = bcm2711PeripheryBaseAddr + 0x20C000
bcm2711GpioAddr = bcm2711PeripheryBaseAddr + 0x200000
bcm2711ClkAddr = bcm2711PeripheryBaseAddr + 0x101000
bcm2711ClkManagerPwd = (0x5A << 24) //(31 - 24) on CM_GP0CTL/CM_GP1CTL/CM_GP2CTL regs
bcm2711PageSize = 4096 // theoretical page size
// FIXME: no dead code
//bcm2711FrontButtonPin = 20
//bcm2711StealthPin = 21
//bcm2711RegPwmTachPin = 13
bcm2711RegGpfsel1 = 0x01
bcm2711RegPwmCtl = 0x00
bcm2711RegPwmRng1 = 0x04
bcm2711RegPwmFif1 = 0x06
bcm2711RegPwmCtlBitPwen2 = 8 // Enable (pwm2)
bcm2711RegPwmCtlBitClrf1 = 6 // Clear FIFO
bcm2711RegPwmCtlBitUsef1 = 5 // Use FIFO
bcm2711RegPwmCtlBitSbit1 = 3 // Line level when not transmitting
bcm2711RegPwmCtlBitRptl1 = 2 // Repeat last data when FIFO is empty
bcm2711RegPwmCtlBitMode1 = 1 // Mode; 0: PWM, 1: Serializer
bcm2711RegPwmCtlBitPwen1 = 0 // Enable (pwm1)
bcm2711RegPwmclkCntrl = 0x28
bcm2711RegPwmclkDiv = 0x29
bcm2711RegPwmclkCntrlBitSrcOsc = 0
bcm2711RegPwmclkCntrlBitEnable = 4
bcm2711DebounceInterval = 100 * time.Millisecond
bcm2711ThermalZonePath = "/sys/class/thermal/thermal_zone0/temp"
smartFanUnitDev = "/dev/ttyAMA5" // UART5
)
type bcm2711 struct {
// Config options
opts ComputeBladeHalOpts
wrMutex sync.Mutex
// Keep track of the currently set fanspeed so it can later be restored after setting the ws281x LEDs
currFanSpeed uint8
devmem *os.File
gpioMem8 []uint8
gpioMem []uint32
pwmMem8 []uint8
pwmMem []uint32
clkMem8 []uint8
clkMem []uint32
gpioChip0 *gpiod.Chip
// Save LED colors so the pixels can be updated individually
leds [2]led.Color
// Stealth mode output
stealthModeLine *gpiod.Line
// Edge button input
edgeButtonLine *gpiod.Line
edgeButtonDebounceChan chan struct{}
edgeButtonWatchChan chan struct{}
// PoE detection input
poeLine *gpiod.Line
// Fan unit
fanUnit FanUnit
}
func NewCm4Hal(ctx context.Context, opts ComputeBladeHalOpts) (ComputeBladeHal, error) {
// /dev/gpiomem doesn't allow complex operations for PWM fan control or WS281x
devmem, err := os.OpenFile("/dev/mem", os.O_RDWR|os.O_SYNC, os.ModePerm)
if err != nil {
return nil, err
}
gpioChip0, err := gpiod.NewChip("gpiochip0")
if err != nil {
return nil, err
}
// Setup memory mappings
gpioMem, gpioMem8, err := mmap(devmem, bcm2711GpioAddr, bcm2711PageSize)
if err != nil {
return nil, err
}
pwmMem, pwmMem8, err := mmap(devmem, bcm2711RegPwmAddr, bcm2711PageSize)
if err != nil {
return nil, err
}
clkMem, clkMem8, err := mmap(devmem, bcm2711ClkAddr, bcm2711PageSize)
if err != nil {
return nil, err
}
bcm := &bcm2711{
devmem: devmem,
gpioMem: gpioMem,
gpioMem8: gpioMem8,
pwmMem: pwmMem,
pwmMem8: pwmMem8,
clkMem: clkMem,
clkMem8: clkMem8,
gpioChip0: gpioChip0,
opts: opts,
edgeButtonDebounceChan: make(chan struct{}, 1),
edgeButtonWatchChan: make(chan struct{}),
}
computeModule.WithLabelValues("cm4").Set(1)
log.FromContext(ctx).Info("starting hal setup", zap.String("hal", "bcm2711"))
err = bcm.setup(ctx)
if err != nil {
return nil, err
}
return bcm, nil
}
// Close cleans all memory mappings
func (bcm *bcm2711) Close() error {
errs := errors.Join(
bcm.fanUnit.Close(),
syscall.Munmap(bcm.gpioMem8),
syscall.Munmap(bcm.pwmMem8),
syscall.Munmap(bcm.clkMem8),
bcm.devmem.Close(),
bcm.gpioChip0.Close(),
bcm.poeLine.Close(),
bcm.stealthModeLine.Close(),
)
return errs
}
// Init initialises GPIOs and sets sane defaults
func (bcm *bcm2711) setup(ctx context.Context) error {
var err error
// Register edge event handler for edge button
bcm.edgeButtonLine, err = bcm.gpioChip0.RequestLine(
rpi.GPIO20, gpiod.WithEventHandler(bcm.handleEdgeButtonEdge),
gpiod.WithFallingEdge, gpiod.WithPullUp, gpiod.WithDebounce(50*time.Millisecond))
if err != nil {
return err
}
// Register input for PoE detection
bcm.poeLine, err = bcm.gpioChip0.RequestLine(rpi.GPIO23, gpiod.AsInput, gpiod.WithPullUp)
if err != nil {
return err
}
// Register output for stealth mode
bcm.stealthModeLine, err = bcm.gpioChip0.RequestLine(rpi.GPIO21, gpiod.AsOutput(1))
if err != nil {
return err
}
// Setup correct fan unit
log.FromContext(ctx).Info("detecting fan unit")
detectCtx, cancel := context.WithTimeout(ctx, 3*time.Second) // temp events are sent every 2 seconds
defer cancel()
if smartFanUnitPresent, err := SmartFanUnitPresent(detectCtx, smartFanUnitDev); err == nil && smartFanUnitPresent {
log.FromContext(ctx).Info("detected smart fan unit")
bcm.fanUnit, err = NewSmartFanUnit(smartFanUnitDev)
if err != nil {
return err
}
} else {
log.FromContext(ctx).WithError(err).Info("no smart fan unit detected, assuming standard fan unit")
// FAN PWM output for standard fan unit (GPIO 12)
// -> bcm2711RegGpfsel1 8:6, alt0
bcm.gpioMem[bcm2711RegGpfsel1] = (bcm.gpioMem[bcm2711RegGpfsel1] &^ (0b111 << 6)) | (0b100 << 6)
bcm.fanUnit = &standardFanUnitBcm2711{
GpioChip0: bcm.gpioChip0,
DisableRpmReporting: !bcm.opts.RpmReportingStandardFanUnit,
SetFanSpeedPwmFunc: func(speed uint8) error {
bcm.setFanSpeedPWM(speed)
return nil
},
}
}
return nil
}
func (bcm *bcm2711) Run(parentCtx context.Context) error {
ctx, cancel := context.WithCancel(parentCtx)
defer cancel()
group := errgroup.Group{}
group.Go(func() error {
defer cancel()
return bcm.fanUnit.Run(ctx)
})
return group.Wait()
}
func (bcm *bcm2711) handleEdgeButtonEdge(evt gpiod.LineEvent) {
// Despite debouncing, we still get multiple events for a single button press
// -> This is an in-software debounce to ensure we only get one event per button press
select {
case bcm.edgeButtonDebounceChan <- struct{}{}:
go func() {
// Manually debounce the button
<-bcm.edgeButtonDebounceChan
time.Sleep(bcm2711DebounceInterval)
edgeButtonEventCount.Inc()
close(bcm.edgeButtonWatchChan)
bcm.edgeButtonWatchChan = make(chan struct{})
}()
default:
// noop
return
}
}
// WaitForEdgeButtonPress blocks until the edge button has been pressed
func (bcm *bcm2711) WaitForEdgeButtonPress(parentCtx context.Context) error {
ctx, cancel := context.WithCancel(parentCtx)
defer cancel()
fanUnitChan := make(chan struct{})
go func() {
err := bcm.fanUnit.WaitForButtonPress(ctx)
if err != nil && err != context.Canceled {
log.FromContext(ctx).WithError(err).Error("failed to wait for button press")
} else {
close(fanUnitChan)
}
}()
// Either wait for the context to be cancelled or the edge button to be pressed
select {
case <-ctx.Done():
return ctx.Err()
case <-bcm.edgeButtonWatchChan:
return nil
case <-fanUnitChan:
return nil
}
}
func (bcm *bcm2711) GetFanRPM() (float64, error) {
rpm, err := bcm.fanUnit.FanSpeedRPM(context.TODO())
return float64(rpm), err
}
func (bcm *bcm2711) GetPowerStatus() (PowerStatus, error) {
// GPIO 23 is used for PoE detection
val, err := bcm.poeLine.Value()
if err != nil {
return PowerPoeOrUsbC, err
}
if val > 0 {
powerStatus.WithLabelValues(fmt.Sprint(PowerPoe802at)).Set(1)
powerStatus.WithLabelValues(fmt.Sprint(PowerPoeOrUsbC)).Set(0)
return PowerPoe802at, nil
}
powerStatus.WithLabelValues(fmt.Sprint(PowerPoe802at)).Set(0)
powerStatus.WithLabelValues(fmt.Sprint(PowerPoeOrUsbC)).Set(1)
return PowerPoeOrUsbC, nil
}
func (bcm *bcm2711) setPwm0Freq(targetFrequency uint64) error {
// Calculate PWM divisor based on target frequency
divisor := 54000000 / targetFrequency
realDivisor := divisor & 0xfff // 12 bits
if divisor != realDivisor {
return fmt.Errorf("invalid frequency, max divisor is 4095, calculated divisor is %d", divisor)
}
// Stop pwm for both channels; this is required to set the new configuration
bcm.pwmMem[bcm2711RegPwmCtl] &^= (1 << bcm2711RegPwmCtlBitPwen1) | (1 << bcm2711RegPwmCtlBitPwen2)
time.Sleep(time.Microsecond * 10)
// Stop clock w/o any changes, they cannot be made in the same step
bcm.clkMem[bcm2711RegPwmclkCntrl] = bcm2711ClkManagerPwd | (bcm.clkMem[bcm2711RegPwmclkCntrl] &^ (1 << 4))
time.Sleep(time.Microsecond * 10)
// Wait for the clock to not be busy so we can perform the changes
for bcm.clkMem[bcm2711RegPwmclkCntrl]&(1<<7) != 0 {
time.Sleep(time.Microsecond * 10)
}
// passwd, disabled, source (oscillator)
bcm.clkMem[bcm2711RegPwmclkCntrl] = bcm2711ClkManagerPwd | (0 << bcm2711RegPwmclkCntrlBitEnable) | (1 << bcm2711RegPwmclkCntrlBitSrcOsc)
time.Sleep(time.Microsecond * 10)
bcm.clkMem[bcm2711RegPwmclkDiv] = bcm2711ClkManagerPwd | (uint32(divisor) << 12)
time.Sleep(time.Microsecond * 10)
// Start clock (passwd, enable, source)
bcm.clkMem[bcm2711RegPwmclkCntrl] = bcm2711ClkManagerPwd | (1 << bcm2711RegPwmclkCntrlBitEnable) | (1 << bcm2711RegPwmclkCntrlBitSrcOsc)
time.Sleep(time.Microsecond * 10)
// Start pwm for both channels again
bcm.pwmMem[bcm2711RegPwmCtl] &= (1 << bcm2711RegPwmCtlBitPwen1)
time.Sleep(time.Microsecond * 10)
return nil
}
// SetFanSpeed sets the fanspeed of a blade in percent (standard fan unit)
func (bcm *bcm2711) SetFanSpeed(speed uint8) error {
fanTargetPercent.Set(float64(speed))
return bcm.fanUnit.SetFanSpeedPercent(context.TODO(), speed)
}
func (bcm *bcm2711) setFanSpeedPWM(speed uint8) {
// Noctua fans are expecting a 25khz signal, where duty cycle controls fan on/speed/off
// With the usage of the FIFO, we can alter the duty cycle by the number of bits set in the FIFO, maximum of 32.
// We therefore need a frequency of 32*25khz = 800khz, which is a divisor of 67.5 (thus we'll use 68).
// This results in an actual period frequency of 24.8khz, which is within the specifications of Noctua fans.
err := bcm.setPwm0Freq(800000)
if err != nil {
// we know it produces a valid divisor, so this should never happen
panic(err)
}
// Using hardware ticks would offer a better resultion, but this works for now.
var targetvalue uint32 = 0
if speed == 0 {
targetvalue = 0
} else if speed <= 100 {
for i := 0; i <= int((float64(speed)/100.0)*32.0); i++ {
targetvalue |= (1 << i)
}
} else {
targetvalue = ^(uint32(0))
}
// Use fifo, repeat, ...
bcm.pwmMem[bcm2711RegPwmCtl] = (1 << bcm2711RegPwmCtlBitPwen1) | (1 << bcm2711RegPwmCtlBitMode1) | (1 << bcm2711RegPwmCtlBitRptl1) | (1 << bcm2711RegPwmCtlBitUsef1)
time.Sleep(10 * time.Microsecond)
bcm.pwmMem[bcm2711RegPwmRng1] = 32
time.Sleep(10 * time.Microsecond)
bcm.pwmMem[bcm2711RegPwmFif1] = targetvalue
// Store fan speed for later use
bcm.currFanSpeed = speed
}
func (bcm *bcm2711) SetStealthMode(enable bool) error {
if enable {
stealthModeEnabled.Set(1)
return bcm.stealthModeLine.SetValue(1)
} else {
stealthModeEnabled.Set(0)
return bcm.stealthModeLine.SetValue(0)
}
}
func (bcm *bcm2711) StealthModeActive() bool {
val, err := bcm.stealthModeLine.Value()
if err != nil {
return false
}
return val > 0
}
// serializePwmDataFrame converts a byte to a 24 bit PWM data frame for WS281x LEDs
func serializePwmDataFrame(data uint8) uint32 {
var result uint32 = 0
for i := 7; i >= 0; i-- {
if i != 7 {
result <<= 3
}
if (uint32(data)&(1<<i))>>i == 0 {
result |= 0b100 // -__
} else {
result |= 0b110 // --_
}
}
return result
}
func (bcm *bcm2711) SetLed(idx LedIndex, color led.Color) error {
if idx >= 2 {
return fmt.Errorf("invalid led index %d, supported: [0, 1]", idx)
}
// Update the fan unit LED if the index is the same as the fan unit LED index
if idx == LedEdge {
if err := bcm.fanUnit.SetLed(context.TODO(), color); err != nil {
return err
}
}
bcm.leds[idx] = color
return bcm.updateLEDs()
}
// updateLEDs sets the color of the WS281x LEDs
func (bcm *bcm2711) updateLEDs() error {
bcm.wrMutex.Lock()
defer bcm.wrMutex.Unlock()
ledColorChangeEventCount.Inc()
// Set frequency to 3*800khz.
// we'll bit-bang the data, so we'll need to send 3 bits per one bit of data.
if err := bcm.setPwm0Freq(3 * 800000); err != nil {
return err
}
time.Sleep(10 * time.Microsecond)
// WS281x Output (GPIO 18)
// -> bcm2711RegGpfsel1 24:26, regular output; it's configured as alt5 whenever pixel data is sent.
// This is not optimal but required as the pwm0 peripheral is shared between fan and data line for the LEDs.
time.Sleep(10 * time.Microsecond)
bcm.gpioMem[bcm2711RegGpfsel1] = (bcm.gpioMem[bcm2711RegGpfsel1] &^ (0b111 << 24)) | (0b010 << 24)
time.Sleep(10 * time.Microsecond)
defer func() {
// Set to regular output again so the PWM signal doesn't confuse the WS2812
bcm.gpioMem[bcm2711RegGpfsel1] = (bcm.gpioMem[bcm2711RegGpfsel1] &^ (0b111 << 24)) | (0b001 << 24)
bcm.setFanSpeedPWM(bcm.currFanSpeed)
}()
bcm.pwmMem[bcm2711RegPwmCtl] = (1 << bcm2711RegPwmCtlBitMode1) | (1 << bcm2711RegPwmCtlBitRptl1) | (0 << bcm2711RegPwmCtlBitSbit1) | (1 << bcm2711RegPwmCtlBitUsef1) | (1 << bcm2711RegPwmCtlBitClrf1)
time.Sleep(10 * time.Microsecond)
// bcm.pwmMem[bcm2711RegPwmRng1] = 32
bcm.pwmMem[bcm2711RegPwmRng1] = 24 // we only need 24 bits per LED
time.Sleep(10 * time.Microsecond)
// Add sufficient padding to clear 50us of silence with ~412.5ns per bit -> at least 121 bits -> let's be safe and send 6*24=144 bits of silence
bcm.pwmMem[bcm2711RegPwmFif1] = 0
bcm.pwmMem[bcm2711RegPwmFif1] = 0
bcm.pwmMem[bcm2711RegPwmFif1] = 0
bcm.pwmMem[bcm2711RegPwmFif1] = 0
bcm.pwmMem[bcm2711RegPwmFif1] = 0
bcm.pwmMem[bcm2711RegPwmFif1] = 0
// Write top LED data
bcm.pwmMem[bcm2711RegPwmFif1] = serializePwmDataFrame(bcm.leds[0].Red) << 8
bcm.pwmMem[bcm2711RegPwmFif1] = serializePwmDataFrame(bcm.leds[0].Green) << 8
bcm.pwmMem[bcm2711RegPwmFif1] = serializePwmDataFrame(bcm.leds[0].Blue) << 8
// Write edge LED data
bcm.pwmMem[bcm2711RegPwmFif1] = serializePwmDataFrame(bcm.leds[1].Red) << 8
bcm.pwmMem[bcm2711RegPwmFif1] = serializePwmDataFrame(bcm.leds[1].Green) << 8
bcm.pwmMem[bcm2711RegPwmFif1] = serializePwmDataFrame(bcm.leds[1].Blue) << 8
// make sure there's >50us of silence
bcm.pwmMem[bcm2711RegPwmFif1] = 0 // auto-repeated, so no need to feed the FIFO further.
bcm.pwmMem[bcm2711RegPwmCtl] = (1 << bcm2711RegPwmCtlBitPwen1) | (1 << bcm2711RegPwmCtlBitMode1) | (1 << bcm2711RegPwmCtlBitRptl1) | (0 << bcm2711RegPwmCtlBitSbit1) | (1 << bcm2711RegPwmCtlBitUsef1)
// sleep for 4*50us to ensure the data is sent. This is probably a bit too gracious but does not have a significant impact, so let's be safe data gets out.
time.Sleep(200 * time.Microsecond)
return nil
}
// GetTemperature returns the current temperature of the SoC
func (bcm *bcm2711) GetTemperature() (float64, error) {
// Read temperature
f, err := os.Open(bcm2711ThermalZonePath)
if err != nil {
return -1, err
}
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
}