mirror of
https://github.com/compute-blade-community/compute-blade-agent.git
synced 2026-04-16 07:25:41 +02:00
feat: add smart fan unit support (#29)
* feat: add smart fanunit (serial) protocol Signed-off-by: Matthias Riegler <matthias.riegler@ankorstore.com> * feat: add rudimentary eventbus to ease implementation Signed-off-by: Matthias Riegler <matthias.riegler@ankorstore.com> * feat: smart fanunit client Signed-off-by: Matthias Riegler <matthias.riegler@ankorstore.com> * feat: initial smart fan unit implementation Signed-off-by: Matthias Riegler <matthias.riegler@ankorstore.com> * feat: improve logging, double btn press Signed-off-by: Matthias Riegler <matthias.riegler@ankorstore.com> * fix: testcases Signed-off-by: Matthias Riegler <matthias.riegler@ankorstore.com> * fix: context closure handling, RPM reporting Signed-off-by: Matthias Riegler <matthias.riegler@ankorstore.com> * fix: address linting issues Signed-off-by: Matthias Riegler <matthias.riegler@ankorstore.com> * fix: edge line closure Signed-off-by: Matthias Riegler <matthias.riegler@ankorstore.com> * fix: reset CPU after i2c lockup Signed-off-by: Matthias Riegler <matthias.riegler@ankorstore.com> * feat: add uf2 to release Signed-off-by: Matthias Riegler <matthias.riegler@ankorstore.com> --------- Signed-off-by: Matthias Riegler <matthias.riegler@ankorstore.com>
This commit is contained in:
11
.github/workflows/release.yaml
vendored
11
.github/workflows/release.yaml
vendored
@@ -52,6 +52,11 @@ jobs:
|
||||
path: ${{ steps.go-cache-paths.outputs.go-mod }}
|
||||
key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }}
|
||||
|
||||
# Setup tinygo
|
||||
- uses: acifani/setup-tinygo@v2
|
||||
with:
|
||||
tinygo-version: '0.30.0'
|
||||
|
||||
# Setup docker buildx
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
@@ -68,8 +73,12 @@ jobs:
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@v3
|
||||
|
||||
# Build fanunit firmware
|
||||
- name: Build FanUnit Firmware
|
||||
run: make build-fanunit
|
||||
|
||||
# Run goreleaser
|
||||
- name: Goreleaser
|
||||
- name: Run Goreleaser
|
||||
uses: goreleaser/goreleaser-action@v5
|
||||
with:
|
||||
version: latest
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -3,3 +3,5 @@ dist/
|
||||
|
||||
*.test
|
||||
*.out
|
||||
cover.cov
|
||||
fanunit.u2f
|
||||
|
||||
@@ -79,7 +79,12 @@ nfpms:
|
||||
contents:
|
||||
- src: ./hack/systemd/computeblade-agent.service
|
||||
dst: /etc/systemd/system/computeblade-agent.service
|
||||
type: config
|
||||
- src: ./cmd/agent/default-config.yaml
|
||||
dst: /etc/computeblade-agent/config.yaml
|
||||
type: config
|
||||
type: config|noreplace
|
||||
- src: ./fanunit.uf2
|
||||
dst: /usr/share/computeblade-agent/fanunit.uf2
|
||||
|
||||
release:
|
||||
extra_files:
|
||||
- glob: ./fanunit.uf2
|
||||
|
||||
23
Makefile
23
Makefile
@@ -1,5 +1,6 @@
|
||||
FUZZ_TARGETS := ./pkg/smartfanunit/proto
|
||||
|
||||
all: lint
|
||||
all: lint test
|
||||
|
||||
.PHONY: run
|
||||
run:
|
||||
@@ -13,15 +14,29 @@ lint:
|
||||
test:
|
||||
go test ./... -v
|
||||
|
||||
|
||||
.PHONY: fuzz
|
||||
fuzz:
|
||||
@for target in $(FUZZ_TARGETS); do \
|
||||
go test -fuzz="Fuzz" -fuzztime=5s -fuzzminimizetime=10s $$target; \
|
||||
done
|
||||
|
||||
|
||||
.PHONY: generate
|
||||
generate: buf
|
||||
$(BUF) generate
|
||||
|
||||
release:
|
||||
goreleaser release --clean
|
||||
.PHONY: build-fanunit
|
||||
build-fanunit:
|
||||
tinygo build -target=pico -o fanunit.uf2 ./cmd/fanunit/
|
||||
|
||||
.PHONY: build-agent
|
||||
build-agent: generate
|
||||
goreleaser build --snapshot --clean
|
||||
|
||||
.PHONY: snapshot
|
||||
snapshot:
|
||||
goreleaser release --snapshot --skip-publish --clean
|
||||
goreleaser release --snapshot --skip=publish --clean
|
||||
|
||||
# Dependencies
|
||||
LOCALBIN ?= $(shell pwd)/bin
|
||||
|
||||
@@ -10,13 +10,12 @@ listen:
|
||||
|
||||
# Hardware abstraction layer configuration
|
||||
hal:
|
||||
bcm2711:
|
||||
# For the default fan unit, fanspeed measurement is causing a tiny bit of CPU laod.
|
||||
# Sometimes it might not be desired
|
||||
disable_fanspeed_measurement: false
|
||||
# For the default fan unit, fanspeed measurement is causing a tiny bit of CPU laod.
|
||||
# Sometimes it might not be desired
|
||||
rpm_reporting_standard_fan_unit: true
|
||||
|
||||
# Idle LED color, values range from 0-255
|
||||
idle_led_color:
|
||||
idle_led_color:
|
||||
red: 0
|
||||
green: 16
|
||||
blue: 0
|
||||
|
||||
@@ -36,7 +36,7 @@ func main() {
|
||||
// Setup configuration
|
||||
viper.SetConfigType("yaml")
|
||||
// auto-bind environment variables
|
||||
viper.SetEnvPrefix("AGENT")
|
||||
viper.SetEnvPrefix("BLADE")
|
||||
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
viper.AutomaticEnv()
|
||||
// Load potential file configs
|
||||
@@ -56,7 +56,9 @@ func main() {
|
||||
}
|
||||
|
||||
zapLogger := baseLogger.With(zap.String("app", "computeblade-agent"))
|
||||
defer zapLogger.Sync()
|
||||
defer func() {
|
||||
_ = zapLogger.Sync()
|
||||
}()
|
||||
_ = zap.ReplaceGlobals(zapLogger.With(zap.String("scope", "global")))
|
||||
baseCtx := log.IntoContext(context.Background(), zapLogger)
|
||||
|
||||
@@ -70,12 +72,6 @@ func main() {
|
||||
cancelCtx(err)
|
||||
}
|
||||
|
||||
computebladeAgent, err := agent.NewComputeBladeAgent(cbAgentConfig)
|
||||
if err != nil {
|
||||
log.FromContext(ctx).Error("Failed to create agent", zap.Error(err))
|
||||
cancelCtx(err)
|
||||
}
|
||||
|
||||
// setup stop signal handlers
|
||||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||
@@ -91,10 +87,19 @@ func main() {
|
||||
}
|
||||
}()
|
||||
|
||||
log.FromContext(ctx).Info("Bootstrapping computeblade-agent", zap.String("version", viper.GetString("version")))
|
||||
computebladeAgent, err := agent.NewComputeBladeAgent(ctx, cbAgentConfig)
|
||||
if err != nil {
|
||||
log.FromContext(ctx).Error("Failed to create agent", zap.Error(err))
|
||||
cancelCtx(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Run agent
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
log.FromContext(ctx).Info("Starting agent")
|
||||
err := computebladeAgent.Run(ctx)
|
||||
if err != nil && err != context.Canceled {
|
||||
log.FromContext(ctx).Error("Failed to run agent", zap.Error(err))
|
||||
|
||||
@@ -45,18 +45,6 @@ func clientFromContext(ctx context.Context) bladeapiv1alpha1.BladeAgentServiceCl
|
||||
return client
|
||||
}
|
||||
|
||||
func grpcConnIntoContext(ctx context.Context, grpcConn *grpc.ClientConn) context.Context {
|
||||
return context.WithValue(ctx, defaultGrpcClientConnContextKey, grpcConn)
|
||||
}
|
||||
|
||||
func grpcConnFromContext(ctx context.Context) *grpc.ClientConn {
|
||||
grpcConn, ok := ctx.Value(defaultGrpcClientContextKey).(*grpc.ClientConn)
|
||||
if !ok {
|
||||
panic("grpc client connection not found in context")
|
||||
}
|
||||
return grpcConn
|
||||
}
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "bladectl",
|
||||
Short: "bladectl interacts with the computeblade-agent and allows you to manage hardware-features of your compute blade(s)",
|
||||
@@ -85,9 +73,7 @@ var rootCmd = &cobra.Command{
|
||||
}
|
||||
client := bladeapiv1alpha1.NewBladeAgentServiceClient(conn)
|
||||
|
||||
cmd.SetContext(
|
||||
grpcConnIntoContext(clientIntoContext(ctx, client), conn),
|
||||
)
|
||||
cmd.SetContext( clientIntoContext(ctx, client))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
261
cmd/fanunit/controller.go
Normal file
261
cmd/fanunit/controller.go
Normal file
@@ -0,0 +1,261 @@
|
||||
//go:build tinygo
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"machine"
|
||||
"time"
|
||||
|
||||
"github.com/xvzf/computeblade-agent/pkg/eventbus"
|
||||
"github.com/xvzf/computeblade-agent/pkg/hal/led"
|
||||
"github.com/xvzf/computeblade-agent/pkg/smartfanunit"
|
||||
"github.com/xvzf/computeblade-agent/pkg/smartfanunit/emc2101"
|
||||
"github.com/xvzf/computeblade-agent/pkg/smartfanunit/proto"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"tinygo.org/x/drivers"
|
||||
"tinygo.org/x/drivers/ws2812"
|
||||
)
|
||||
|
||||
const (
|
||||
leftBladeTopicIn = "left:in"
|
||||
leftBladeTopicOut = "left:out"
|
||||
rightBladeTopicIn = "right:in"
|
||||
rightBladeTopicOut = "right:out"
|
||||
)
|
||||
|
||||
type Controller struct {
|
||||
DefaultFanSpeed uint8
|
||||
LEDs ws2812.Device
|
||||
FanController emc2101.EMC2101
|
||||
ButtonPin machine.Pin
|
||||
|
||||
LeftUART drivers.UART
|
||||
RightUART drivers.UART
|
||||
|
||||
eb eventbus.EventBus
|
||||
leftLed led.Color
|
||||
rightLed led.Color
|
||||
leftReqFanSpeed uint8
|
||||
rightReqFanSpeed uint8
|
||||
|
||||
buttonPressed int
|
||||
}
|
||||
|
||||
func (c *Controller) Run(parentCtx context.Context) error {
|
||||
c.eb = eventbus.New()
|
||||
|
||||
c.FanController.Init()
|
||||
c.FanController.SetFanPercent(c.DefaultFanSpeed)
|
||||
c.LEDs.Write([]byte{0, 0, 0, 0, 0, 0})
|
||||
|
||||
group, ctx := errgroup.WithContext(parentCtx)
|
||||
|
||||
// LED Update events
|
||||
println("[+] Starting LED update loop")
|
||||
group.Go(func() error {
|
||||
return c.updateLEDs(ctx)
|
||||
})
|
||||
|
||||
// Fan speed update events
|
||||
println("[+] Starting fan update loop")
|
||||
group.Go(func() error {
|
||||
return c.updateFanSpeed(ctx)
|
||||
})
|
||||
|
||||
// Metric reporting events
|
||||
println("[+] Starting metric reporting loop")
|
||||
group.Go(func() error {
|
||||
return c.metricReporter(ctx);
|
||||
})
|
||||
|
||||
// Left blade events
|
||||
println("[+] Starting event listener (left)")
|
||||
group.Go(func() error {
|
||||
return c.listenEvents(ctx, c.LeftUART, leftBladeTopicIn)
|
||||
})
|
||||
println("[+] Starting event dispatcher (left)")
|
||||
group.Go(func() error {
|
||||
return c.dispatchEvents(ctx, c.LeftUART, leftBladeTopicOut)
|
||||
})
|
||||
|
||||
// right blade events
|
||||
println("[+] Starting event listener (righ)")
|
||||
group.Go(func() error {
|
||||
return c.listenEvents(ctx, c.RightUART, rightBladeTopicIn)
|
||||
})
|
||||
println("[+] Starting event dispatcher (right)")
|
||||
group.Go(func() error {
|
||||
return c.dispatchEvents(ctx, c.RightUART, rightBladeTopicOut)
|
||||
})
|
||||
|
||||
// Button Press events
|
||||
println("[+] Starting button interrupt handler")
|
||||
c.ButtonPin.SetInterrupt(machine.PinFalling, func(machine.Pin) {
|
||||
c.buttonPressed += 1
|
||||
})
|
||||
|
||||
group.Go(func() error {
|
||||
ticker := time.NewTicker(20 * time.Millisecond)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case <-ticker.C:
|
||||
btnPressed := smartfanunit.ButtonPressPacket{}
|
||||
if c.buttonPressed > 0 {
|
||||
// Allow up to 600ms for a 2nc button press
|
||||
time.Sleep(600 * time.Millisecond)
|
||||
}
|
||||
|
||||
if c.buttonPressed == 1 {
|
||||
println("[ ] Button pressed once")
|
||||
c.eb.Publish(leftBladeTopicOut, btnPressed.Packet())
|
||||
}
|
||||
if c.buttonPressed == 2 {
|
||||
println("[ ] Button pressed twice")
|
||||
c.eb.Publish(rightBladeTopicOut, btnPressed.Packet())
|
||||
}
|
||||
c.buttonPressed = 0
|
||||
}
|
||||
}
|
||||
})
|
||||
return group.Wait()
|
||||
}
|
||||
|
||||
// listenEvents reads events from the UART interface and dispatches them to the eventbus
|
||||
func (c *Controller) listenEvents(ctx context.Context, uart drivers.UART, targetTopic string) error {
|
||||
for {
|
||||
// Read packet from UART; blocks until packet is received
|
||||
pkt, err := proto.ReadPacket(ctx, uart)
|
||||
if err != nil {
|
||||
println("[!] failed to read packet, continuing..", err.Error())
|
||||
continue
|
||||
}
|
||||
println("[ ] received packet from UART publishing to topic", targetTopic)
|
||||
c.eb.Publish(targetTopic, pkt)
|
||||
}
|
||||
}
|
||||
|
||||
// dispatchEvents reads events from the eventbus and writes them to the UART interface
|
||||
func (c *Controller) dispatchEvents(ctx context.Context, uart drivers.UART, sourceTopic string) error {
|
||||
sub := c.eb.Subscribe(sourceTopic, 4, eventbus.MatchAll)
|
||||
defer sub.Unsubscribe()
|
||||
for {
|
||||
select {
|
||||
case msg := <-sub.C():
|
||||
println("[ ] dispatching event to UART from topic", sourceTopic)
|
||||
pkt := msg.(proto.Packet)
|
||||
err := proto.WritePacket(ctx, uart, pkt)
|
||||
if err != nil {
|
||||
println(err.Error())
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) metricReporter(ctx context.Context) error {
|
||||
var err error
|
||||
|
||||
ticker := time.NewTicker(2 * time.Second)
|
||||
airFlowTempRight := smartfanunit.AirFlowTemperaturePacket{}
|
||||
airFlowTempLeft := smartfanunit.AirFlowTemperaturePacket{}
|
||||
fanRpm := smartfanunit.FanSpeedRPMPacket{}
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case <-ticker.C:
|
||||
}
|
||||
|
||||
airFlowTempLeft.Temperature, err = c.FanController.InternalTemperature()
|
||||
if err != nil {
|
||||
println("[!] failed to read internal temperature:", err.Error())
|
||||
}
|
||||
airFlowTempRight.Temperature, err = c.FanController.ExternalTemperature()
|
||||
if err != nil {
|
||||
println("[!] failed to read external temperature:", err.Error())
|
||||
}
|
||||
fanRpm.RPM, err = c.FanController.FanRPM()
|
||||
if err != nil {
|
||||
println("[!] failed to read fan RPM:", err.Error())
|
||||
}
|
||||
|
||||
// FIXME: This is a workaround for an i2c lockup issue.
|
||||
if err != nil {
|
||||
println("[!] resetting CPU")
|
||||
time.Sleep(100*time.Millisecond)
|
||||
machine.CPUReset()
|
||||
}
|
||||
|
||||
// Publish metrics
|
||||
c.eb.Publish(leftBladeTopicOut, airFlowTempLeft.Packet())
|
||||
c.eb.Publish(rightBladeTopicOut, airFlowTempRight.Packet())
|
||||
c.eb.Publish(leftBladeTopicOut, fanRpm.Packet())
|
||||
c.eb.Publish(rightBladeTopicOut, fanRpm.Packet())
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) updateFanSpeed(ctx context.Context) error {
|
||||
var pkt smartfanunit.SetFanSpeedPercentPacket
|
||||
|
||||
subLeft := c.eb.Subscribe(leftBladeTopicIn, 1, eventbus.MatchAll)
|
||||
defer subLeft.Unsubscribe()
|
||||
subRight := c.eb.Subscribe(rightBladeTopicIn, 1, eventbus.MatchAll)
|
||||
defer subRight.Unsubscribe()
|
||||
|
||||
for {
|
||||
// Update LED color depending on blade
|
||||
select {
|
||||
case msg := <-subLeft.C():
|
||||
pkt.FromPacket(msg.(proto.Packet))
|
||||
c.leftReqFanSpeed = pkt.Percent
|
||||
case msg := <-subRight.C():
|
||||
pkt.FromPacket(msg.(proto.Packet))
|
||||
c.rightReqFanSpeed = pkt.Percent
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update fan speed with the max requested speed
|
||||
if c.leftReqFanSpeed > c.rightReqFanSpeed {
|
||||
c.FanController.SetFanPercent(c.leftReqFanSpeed)
|
||||
} else {
|
||||
c.FanController.SetFanPercent(c.rightReqFanSpeed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) updateLEDs(ctx context.Context) error {
|
||||
subLeft := c.eb.Subscribe(leftBladeTopicIn, 1, smartfanunit.MatchCmd(smartfanunit.CmdSetLED))
|
||||
defer subLeft.Unsubscribe()
|
||||
subRight := c.eb.Subscribe(rightBladeTopicIn, 1, smartfanunit.MatchCmd(smartfanunit.CmdSetLED))
|
||||
defer subRight.Unsubscribe()
|
||||
|
||||
var pkt smartfanunit.SetLEDPacket
|
||||
for {
|
||||
// Update LED color depending on blade
|
||||
select {
|
||||
case msg := <-subLeft.C():
|
||||
pkt.FromPacket(msg.(proto.Packet))
|
||||
c.leftLed = pkt.Color
|
||||
case msg := <-subRight.C():
|
||||
pkt.FromPacket(msg.(proto.Packet))
|
||||
c.rightLed = pkt.Color
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
}
|
||||
// Write to LEDs (they are in a chain -> we always have to update both)
|
||||
_, err := c.LEDs.Write([]byte{
|
||||
c.rightLed.Blue, c.rightLed.Green, c.rightLed.Red,
|
||||
c.leftLed.Blue, c.leftLed.Green, c.leftLed.Red,
|
||||
})
|
||||
if err != nil {
|
||||
println("[!] failed to update LEDs", err.Error())
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
87
cmd/fanunit/main.go
Normal file
87
cmd/fanunit/main.go
Normal file
@@ -0,0 +1,87 @@
|
||||
//go:build tinygo
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"machine"
|
||||
"time"
|
||||
|
||||
"github.com/xvzf/computeblade-agent/pkg/smartfanunit"
|
||||
"github.com/xvzf/computeblade-agent/pkg/smartfanunit/emc2101"
|
||||
"tinygo.org/x/drivers/ws2812"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var controller *Controller
|
||||
var emc emc2101.EMC2101
|
||||
var bgrLeds ws2812.Device
|
||||
var err error
|
||||
|
||||
// Configure status LED
|
||||
machine.LED.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
machine.LED.Set(false)
|
||||
|
||||
// Configure UARTs
|
||||
err = machine.UART0.Configure(machine.UARTConfig{TX: machine.UART0_TX_PIN, RX: machine.UART0_RX_PIN})
|
||||
if err != nil {
|
||||
println("[!] Failed to initialize UART0:", err.Error())
|
||||
goto errprint
|
||||
}
|
||||
machine.UART0.SetBaudRate(smartfanunit.Baudrate)
|
||||
err = machine.UART1.Configure(machine.UARTConfig{TX: machine.UART1_TX_PIN, RX: machine.UART1_RX_PIN})
|
||||
if err != nil {
|
||||
println("[!] Failed to initialize UART1:", err.Error())
|
||||
goto errprint
|
||||
}
|
||||
machine.UART1.SetBaudRate(smartfanunit.Baudrate)
|
||||
|
||||
// Enables fan, DO NOT CHANGE
|
||||
machine.GP16.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
machine.GP16.Set(true)
|
||||
|
||||
// WS2812 LEDs
|
||||
machine.GP15.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
bgrLeds = ws2812.New(machine.GP15)
|
||||
|
||||
// Configure button
|
||||
machine.GP12.Configure(machine.PinConfig{Mode: machine.PinInput})
|
||||
|
||||
// Setup emc2101
|
||||
machine.I2C0.Configure(machine.I2CConfig{
|
||||
Frequency: 100 * machine.KHz,
|
||||
SDA: machine.I2C0_SDA_PIN,
|
||||
SCL: machine.I2C0_SCL_PIN,
|
||||
})
|
||||
emc = emc2101.New(machine.I2C0)
|
||||
err = emc.Init()
|
||||
if err != nil {
|
||||
println("[!] Failed to initialize emc2101:", err.Error())
|
||||
goto errprint
|
||||
}
|
||||
|
||||
println("[+] IO initialized, starting controller...")
|
||||
|
||||
// Run controller
|
||||
controller = &Controller{
|
||||
DefaultFanSpeed: 40,
|
||||
LEDs: bgrLeds,
|
||||
FanController: emc,
|
||||
ButtonPin: machine.GP12,
|
||||
LeftUART: machine.UART0,
|
||||
RightUART: machine.UART1,
|
||||
}
|
||||
|
||||
err = controller.Run(context.Background())
|
||||
|
||||
// Blinking -> something went wrong
|
||||
errprint:
|
||||
ledState := false
|
||||
for {
|
||||
ledState = !ledState
|
||||
machine.LED.Set(ledState)
|
||||
// Repeat error message
|
||||
println("[FATAL] controller exited with error:", err)
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
BIN
fanunit.uf2
Normal file
BIN
fanunit.uf2
Normal file
Binary file not shown.
12
go.mod
12
go.mod
@@ -1,24 +1,31 @@
|
||||
module github.com/xvzf/computeblade-agent
|
||||
|
||||
go 1.20
|
||||
go 1.21
|
||||
|
||||
toolchain go1.21.3
|
||||
|
||||
require (
|
||||
github.com/jacobsa/go-serial v0.0.0-20180131005756-15cf729a72d4
|
||||
github.com/prometheus/client_golang v1.16.0
|
||||
github.com/spf13/cobra v1.6.1
|
||||
github.com/spf13/viper v1.16.0
|
||||
github.com/stretchr/testify v1.8.3
|
||||
github.com/warthog618/gpiod v0.8.1
|
||||
go.uber.org/zap v1.24.0
|
||||
golang.org/x/sync v0.2.0
|
||||
google.golang.org/grpc v1.56.2
|
||||
google.golang.org/protobuf v1.31.0
|
||||
tinygo.org/x/drivers v0.26.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/creack/goselect v0.1.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
@@ -35,10 +42,11 @@ require (
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/stretchr/objx v0.5.0 // indirect
|
||||
github.com/subosito/gotenv v1.4.2 // indirect
|
||||
go.bug.st/serial v1.6.1 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/net v0.10.0 // indirect
|
||||
golang.org/x/sys v0.10.0 // indirect
|
||||
golang.org/x/sys v0.13.0 // indirect
|
||||
golang.org/x/text v0.9.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
|
||||
23
go.sum
23
go.sum
@@ -39,6 +39,7 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
@@ -52,6 +53,8 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX
|
||||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0=
|
||||
github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -62,6 +65,7 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
|
||||
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
@@ -108,6 +112,7 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
@@ -122,6 +127,8 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe
|
||||
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
@@ -135,15 +142,19 @@ github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:
|
||||
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jacobsa/go-serial v0.0.0-20180131005756-15cf729a72d4 h1:G2ztCwXov8mRvP0ZfjE6nAlaCX2XbykaeHdbT6KwDz0=
|
||||
github.com/jacobsa/go-serial v0.0.0-20180131005756-15cf729a72d4/go.mod h1:2RvX5ZjVtsznNZPEt4xwJXNJrM3VTZoQf7V6gk0ysvs=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||
@@ -153,6 +164,7 @@ github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
||||
github.com/pilebones/go-udev v0.9.0 h1:N1uEO/SxUwtIctc0WLU0t69JeBxIYEYnj8lT/Nabl9Q=
|
||||
github.com/pilebones/go-udev v0.9.0/go.mod h1:T2eI2tUSK0hA2WS5QLjXJUfQkluZQu+18Cqvem3CaXI=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
|
||||
@@ -169,6 +181,7 @@ github.com/prometheus/procfs v0.11.0 h1:5EAgkfkMl659uZPbe9AS2N68a7Cc1TJbPEuGzFuR
|
||||
github.com/prometheus/procfs v0.11.0/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
|
||||
github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
|
||||
@@ -202,6 +215,8 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.bug.st/serial v1.6.1 h1:VSSWmUxlj1T/YlRo2J104Zv3wJFrjHIl/T3NeruWAHY=
|
||||
go.bug.st/serial v1.6.1/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
@@ -211,6 +226,7 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
|
||||
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
|
||||
@@ -308,6 +324,8 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
|
||||
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -346,6 +364,8 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -510,6 +530,7 @@ google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
@@ -527,3 +548,5 @@ honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
tinygo.org/x/drivers v0.26.0 h1:7KSIYssX0ki0dd7yBYkVZWSG0kt8vrZNS0It73TymcA=
|
||||
tinygo.org/x/drivers v0.26.0/go.mod h1:X7utcg3yfFUFuKLOMTZD56eztXMjpkcf8OHldfTBsjw=
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"github.com/xvzf/computeblade-agent/pkg/fancontroller"
|
||||
"github.com/xvzf/computeblade-agent/pkg/hal"
|
||||
"github.com/xvzf/computeblade-agent/pkg/hal/led"
|
||||
"github.com/xvzf/computeblade-agent/pkg/ledengine"
|
||||
"github.com/xvzf/computeblade-agent/pkg/log"
|
||||
"go.uber.org/zap"
|
||||
@@ -63,12 +64,12 @@ func (e Event) String() string {
|
||||
|
||||
type ComputeBladeAgentConfig struct {
|
||||
// IdleLedColor is the color of the edge LED when the blade is idle mode
|
||||
IdleLedColor hal.LedColor `mapstructure:"idle_led_color"`
|
||||
IdleLedColor led.Color `mapstructure:"idle_led_color"`
|
||||
// IdentifyLedColor is the color of the edge LED when the blade is in identify mode
|
||||
IdentifyLedColor hal.LedColor `mapstructure:"identify_led_color"`
|
||||
IdentifyLedColor led.Color `mapstructure:"identify_led_color"`
|
||||
// CriticalLedColor is the color of the top(!) LED when the blade is in critical mode.
|
||||
// In the circumstance when >1 blades are in critical mode, the identidy function can be used to find the right blade
|
||||
CriticalLedColor hal.LedColor `mapstructure:"critical_led_color"`
|
||||
CriticalLedColor led.Color `mapstructure:"critical_led_color"`
|
||||
|
||||
// StealthModeEnabled indicates whether stealth mode is enabled
|
||||
StealthModeEnabled bool `mapstructure:"stealth_mode"`
|
||||
@@ -80,6 +81,8 @@ type ComputeBladeAgentConfig struct {
|
||||
FanSpeed *fancontroller.FanOverrideOpts `mapstructure:"fan_speed"`
|
||||
// FanControllerConfig is the configuration of the fan controller
|
||||
FanControllerConfig fancontroller.FanControllerConfig `mapstructure:"fan_controller"`
|
||||
|
||||
ComputeBladeHalOpts hal.ComputeBladeHalOpts `mapstructure:"hal"`
|
||||
}
|
||||
|
||||
// ComputeBladeAgent implements the core-logic of the agent. It is responsible for handling events and interfacing with the hardware.
|
||||
@@ -110,13 +113,11 @@ type computeBladeAgentImpl struct {
|
||||
eventChan chan Event
|
||||
}
|
||||
|
||||
func NewComputeBladeAgent(opts ComputeBladeAgentConfig) (ComputeBladeAgent, error) {
|
||||
func NewComputeBladeAgent(ctx context.Context, opts ComputeBladeAgentConfig) (ComputeBladeAgent, error) {
|
||||
var err error
|
||||
|
||||
// blade, err := hal.NewCm4Hal(hal.ComputeBladeHalOpts{
|
||||
blade, err := hal.NewCm4Hal(hal.ComputeBladeHalOpts{
|
||||
FanUnit: hal.FanUnitStandard, // FIXME: support smart fan unit
|
||||
})
|
||||
blade, err := hal.NewCm4Hal(ctx, opts.ComputeBladeHalOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -171,6 +172,17 @@ func (a *computeBladeAgentImpl) Run(origCtx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Run HAL
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
log.FromContext(ctx).Info("Starting HAL")
|
||||
if err := a.blade.Run(ctx); err != nil && err != context.Canceled {
|
||||
log.FromContext(ctx).Error("HAL failed", zap.Error(err))
|
||||
cancelCtx(err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Start edge button event handler
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
@@ -258,12 +270,15 @@ func (a *computeBladeAgentImpl) cleanup(ctx context.Context) {
|
||||
if err := a.blade.SetFanSpeed(100); err != nil {
|
||||
log.FromContext(ctx).Error("Failed to set fan speed to 100%", zap.Error(err))
|
||||
}
|
||||
if err := a.blade.SetLed(hal.LedEdge, hal.LedColor{}); err != nil {
|
||||
if err := a.blade.SetLed(hal.LedEdge, led.Color{}); err != nil {
|
||||
log.FromContext(ctx).Error("Failed to set edge LED to off", zap.Error(err))
|
||||
}
|
||||
if err := a.blade.SetLed(hal.LedTop, hal.LedColor{}); err != nil {
|
||||
if err := a.blade.SetLed(hal.LedTop, led.Color{}); err != nil {
|
||||
log.FromContext(ctx).Error("Failed to set edge LED to off", zap.Error(err))
|
||||
}
|
||||
if err := a.Close(); err != nil {
|
||||
log.FromContext(ctx).Error("Failed to close blade", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
func (a *computeBladeAgentImpl) handleEvent(ctx context.Context, event Event) error {
|
||||
@@ -306,7 +321,7 @@ func (a *computeBladeAgentImpl) handleEvent(ctx context.Context, event Event) er
|
||||
|
||||
func (a *computeBladeAgentImpl) handleIdentifyActive(ctx context.Context) error {
|
||||
log.FromContext(ctx).Info("Identify active")
|
||||
return a.edgeLedEngine.SetPattern(ledengine.NewBurstPattern(hal.LedColor{}, a.opts.IdentifyLedColor))
|
||||
return a.edgeLedEngine.SetPattern(ledengine.NewBurstPattern(led.Color{}, a.opts.IdentifyLedColor))
|
||||
}
|
||||
|
||||
func (a *computeBladeAgentImpl) handleIdentifyConfirm(ctx context.Context) error {
|
||||
@@ -325,7 +340,7 @@ func (a *computeBladeAgentImpl) handleCriticalActive(ctx context.Context) error
|
||||
|
||||
// Set critical pattern for top LED
|
||||
setPatternTopLedErr := a.topLedEngine.SetPattern(
|
||||
ledengine.NewSlowBlinkPattern(hal.LedColor{}, a.opts.CriticalLedColor),
|
||||
ledengine.NewSlowBlinkPattern(led.Color{}, a.opts.CriticalLedColor),
|
||||
)
|
||||
// Combine errors, but don't stop execution flow for now
|
||||
return errors.Join(setStealthModeError, setPatternTopLedErr)
|
||||
@@ -342,7 +357,7 @@ func (a *computeBladeAgentImpl) handleCriticalReset(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// Set top LED off
|
||||
if err := a.topLedEngine.SetPattern(ledengine.NewStaticPattern(hal.LedColor{})); err != nil {
|
||||
if err := a.topLedEngine.SetPattern(ledengine.NewStaticPattern(led.Color{})); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -356,7 +371,7 @@ func (a *computeBladeAgentImpl) Close() error {
|
||||
// runTopLedEngine runs the top LED engine
|
||||
func (a *computeBladeAgentImpl) runTopLedEngine(ctx context.Context) error {
|
||||
// FIXME the top LED is only used to indicate emergency situations
|
||||
err := a.topLedEngine.SetPattern(ledengine.NewStaticPattern(hal.LedColor{}))
|
||||
err := a.topLedEngine.SetPattern(ledengine.NewStaticPattern(led.Color{}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
102
pkg/eventbus/eventbus.go
Normal file
102
pkg/eventbus/eventbus.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package eventbus
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// EventBus is a simple event bus with topic-based publish/subscribe.
|
||||
// This is, by no means, a performant or complete implementation but for the scope of this project more than sufficient
|
||||
type EventBus interface {
|
||||
Publish(topic string, message any)
|
||||
Subscribe(topic string, bufSize int, filter func(any) bool) Subscriber
|
||||
}
|
||||
|
||||
type Subscriber interface {
|
||||
C() <-chan any
|
||||
Unsubscribe()
|
||||
}
|
||||
|
||||
type eventBus struct {
|
||||
subscribers map[string]map[*subscriber]func(any) bool
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type subscriber struct {
|
||||
mu sync.Mutex
|
||||
ch chan any
|
||||
closed bool
|
||||
}
|
||||
|
||||
func MatchAll(any) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// New returns an initialized EventBus.
|
||||
func New() EventBus {
|
||||
return &eventBus{
|
||||
subscribers: make(map[string]map[*subscriber]func(any) bool),
|
||||
}
|
||||
}
|
||||
|
||||
// Publish a message to a topic (best-effort). Subscribers with a full receive queue are dropped.
|
||||
func (eb *eventBus) Publish(topic string, message any) {
|
||||
eb.mu.Lock()
|
||||
defer eb.mu.Unlock()
|
||||
|
||||
if eb.subscribers[topic] == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if subs, ok := eb.subscribers[topic]; ok {
|
||||
for sub, filter := range subs {
|
||||
sub.mu.Lock()
|
||||
// Clean up closed subscribers
|
||||
if sub.closed {
|
||||
delete(eb.subscribers[topic], sub)
|
||||
continue
|
||||
}
|
||||
|
||||
if filter(message) {
|
||||
// Try to send message, but don't block
|
||||
select {
|
||||
case sub.ch <- message:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
sub.mu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe to a topic with a filter function. Returns a channel with given buffer size.
|
||||
func (eb *eventBus) Subscribe(topic string, bufSize int, filter func(any) bool) Subscriber {
|
||||
eb.mu.Lock()
|
||||
defer eb.mu.Unlock()
|
||||
|
||||
ch := make(chan any, bufSize)
|
||||
|
||||
sub := &subscriber{
|
||||
ch: ch,
|
||||
closed: false,
|
||||
}
|
||||
|
||||
if _, ok := eb.subscribers[topic]; !ok {
|
||||
eb.subscribers[topic] = make(map[*subscriber]func(any) bool)
|
||||
}
|
||||
|
||||
eb.subscribers[topic][sub] = filter
|
||||
|
||||
return sub
|
||||
}
|
||||
|
||||
func (s *subscriber) C() <-chan any {
|
||||
return s.ch
|
||||
}
|
||||
|
||||
func (s *subscriber) Unsubscribe() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
close(s.ch)
|
||||
s.closed = true
|
||||
}
|
||||
76
pkg/eventbus/eventbus_test.go
Normal file
76
pkg/eventbus/eventbus_test.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package eventbus_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/xvzf/computeblade-agent/pkg/eventbus"
|
||||
)
|
||||
|
||||
func TestEventBusManySubscribers(t *testing.T) {
|
||||
eb := eventbus.New()
|
||||
|
||||
// Create a channel and subscribe to a topic without a filter
|
||||
sub0 := eb.Subscribe("topic0", 2, eventbus.MatchAll)
|
||||
assert.Equal(t, cap(sub0.C()), 2)
|
||||
assert.Equal(t, len(sub0.C()), 0)
|
||||
defer sub0.Unsubscribe()
|
||||
|
||||
// Create a channel and subscribe to a topic with a filter
|
||||
sub1 := eb.Subscribe("topic0", 2, func(msg any) bool {
|
||||
return msg.(int) > 5
|
||||
})
|
||||
assert.Equal(t, cap(sub1.C()), 2)
|
||||
assert.Equal(t, len(sub1.C()), 0)
|
||||
defer sub1.Unsubscribe()
|
||||
|
||||
// Create a channel and subscribe to another topic
|
||||
sub2 := eb.Subscribe("topic1", 1, eventbus.MatchAll)
|
||||
assert.Equal(t, cap(sub2.C()), 1)
|
||||
assert.Equal(t, len(sub2.C()), 0)
|
||||
defer sub2.Unsubscribe()
|
||||
|
||||
sub3 := eb.Subscribe("topic1", 0, eventbus.MatchAll)
|
||||
assert.Equal(t, cap(sub3.C()), 0)
|
||||
assert.Equal(t, len(sub3.C()), 0)
|
||||
defer sub3.Unsubscribe()
|
||||
|
||||
|
||||
// Publish some messages
|
||||
eb.Publish("topic0", 10)
|
||||
eb.Publish("topic0", 4)
|
||||
eb.Publish("topic1", "Hello, World!")
|
||||
|
||||
|
||||
// Assert received messages
|
||||
assert.Equal(t, len(sub0.C()), 2)
|
||||
assert.Equal(t, <-sub0.C(), 10)
|
||||
assert.Equal(t, <-sub0.C(), 4)
|
||||
|
||||
assert.Equal(t, len(sub1.C()), 1)
|
||||
assert.Equal(t, <-sub1.C(), 10)
|
||||
|
||||
assert.Equal(t, len(sub2.C()), 1)
|
||||
assert.Equal(t, <-sub2.C(), "Hello, World!")
|
||||
|
||||
// sub3 has no buffer, so it should be empty as there's been no consumer at time of publishing
|
||||
assert.Equal(t, len(sub3.C()), 0)
|
||||
}
|
||||
|
||||
func TestUnsubscribe(t *testing.T) {
|
||||
eb := eventbus.New()
|
||||
|
||||
// Create a channel and subscribe to a topic
|
||||
sub := eb.Subscribe("topic", 2, eventbus.MatchAll)
|
||||
|
||||
// Unsubscribe from the topic
|
||||
sub.Unsubscribe()
|
||||
|
||||
// Try to publish a message after unsubscribing
|
||||
eb.Publish("topic", "This message should not be received")
|
||||
|
||||
// Assert that the channel is closed
|
||||
_, ok := <-sub.C()
|
||||
assert.False(t, ok, "Unsubscribed channel should be closed")
|
||||
|
||||
}
|
||||
49
pkg/hal/example_smartfanunit_test.go
Normal file
49
pkg/hal/example_smartfanunit_test.go
Normal file
@@ -0,0 +1,49 @@
|
||||
//go:build !tinygo
|
||||
|
||||
package hal_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"github.com/xvzf/computeblade-agent/pkg/hal"
|
||||
"github.com/xvzf/computeblade-agent/pkg/hal/led"
|
||||
)
|
||||
|
||||
func ExampleNewSmartFanUnit() {
|
||||
ctx := context.Background()
|
||||
|
||||
client, err := hal.NewSmartFanUnit("/dev/tty.usbmodem11102")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
go func() {
|
||||
err := client.Run(ctx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Set LED color for the blade to red
|
||||
err = client.SetLed(ctx, led.Color{Red: 100, Green: 0, Blue: 0})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Set fanspeed to 20%
|
||||
err = client.SetFanSpeedPercent(ctx, 20)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
tmp, err := client.AirFlowTemperature(ctx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
log.Println("AirflowTemp", tmp)
|
||||
rpm, err := client.FanSpeedRPM(ctx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
log.Println("RPM", rpm)
|
||||
}
|
||||
@@ -3,62 +3,13 @@ package hal
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"github.com/xvzf/computeblade-agent/pkg/hal/led"
|
||||
)
|
||||
|
||||
type FanUnit uint8
|
||||
type FanUnitKind uint8
|
||||
type ComputeModule uint8
|
||||
type PowerStatus uint8
|
||||
|
||||
var (
|
||||
fanSpeedTargetPercent = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: "computeblade",
|
||||
Name: "fan_speed_target_percent",
|
||||
Help: "Target fanspeed in percent",
|
||||
})
|
||||
fanSpeed = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: "computeblade",
|
||||
Name: "fan_speed",
|
||||
Help: "Fan speed in RPM",
|
||||
})
|
||||
socTemperature = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: "computeblade",
|
||||
Name: "soc_temperature",
|
||||
Help: "SoC temperature in °C",
|
||||
})
|
||||
computeModule = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: "computeblade",
|
||||
Name: "compute_modul_present",
|
||||
Help: "Compute module type",
|
||||
}, []string{"type"})
|
||||
ledColorChangeEventCount = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Namespace: "computeblade",
|
||||
Name: "led_color_change_event_count",
|
||||
Help: "Led color change event_count",
|
||||
})
|
||||
powerStatus = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: "computeblade",
|
||||
Name: "power_status",
|
||||
Help: "Power status of the blade",
|
||||
}, []string{"type"})
|
||||
stealthModeEnabled = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: "computeblade",
|
||||
Name: "stealth_mode_enabled",
|
||||
Help: "Stealth mode enabled",
|
||||
})
|
||||
fanUnit = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: "computeblade",
|
||||
Name: "fan_unit",
|
||||
Help: "Fan unit",
|
||||
}, []string{"type"})
|
||||
edgeButtonEventCount = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Namespace: "computeblade",
|
||||
Name: "edge_button_event_count",
|
||||
Help: "Number of edge button presses",
|
||||
})
|
||||
)
|
||||
|
||||
func (p PowerStatus) String() string {
|
||||
switch p {
|
||||
case PowerPoe802at:
|
||||
@@ -71,8 +22,9 @@ func (p PowerStatus) String() string {
|
||||
}
|
||||
|
||||
const (
|
||||
FanUnitStandard = iota
|
||||
FanUnitSmart
|
||||
FanUnitKindStandard = iota
|
||||
FanUnitKindStandardNoRPM
|
||||
FanUnitKindSmart
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -85,18 +37,15 @@ const (
|
||||
LedEdge
|
||||
)
|
||||
|
||||
type LedColor struct {
|
||||
Red uint8 `mapstructure:"red"`
|
||||
Green uint8 `mapstructure:"green"`
|
||||
Blue uint8 `mapstructure:"blue"`
|
||||
}
|
||||
|
||||
type ComputeBladeHalOpts struct {
|
||||
FanUnit FanUnit
|
||||
RpmReportingStandardFanUnit bool `mapstructure:"rpm_reporting_standard_fan_unit"`
|
||||
}
|
||||
|
||||
// ComputeBladeHal abstracts hardware details of the Compute Blade and provides a simple interface
|
||||
type ComputeBladeHal interface {
|
||||
// Run starts background tasks and returns when the context is cancelled or an error occurs
|
||||
Run(ctx context.Context) error
|
||||
// Close closes the ComputeBladeHal
|
||||
Close() error
|
||||
// SetFanSpeed sets the fan speed in percent
|
||||
SetFanSpeed(speed uint8) error
|
||||
@@ -105,7 +54,7 @@ type ComputeBladeHal interface {
|
||||
// SetStealthMode enables/disables stealth mode of the blade (turning on/off the LEDs)
|
||||
SetStealthMode(enabled bool) error
|
||||
// SetLEDs sets the color of the LEDs
|
||||
SetLed(idx uint, color LedColor) error
|
||||
SetLed(idx uint, color led.Color) error
|
||||
// GetPowerStatus returns the current power status of the blade
|
||||
GetPowerStatus() (PowerStatus, error)
|
||||
// GetTemperature returns the current temperature of the SoC in °C
|
||||
@@ -113,3 +62,31 @@ type ComputeBladeHal interface {
|
||||
// GetEdgeButtonPressChan returns a channel emitting edge button press events
|
||||
WaitForEdgeButtonPress(ctx context.Context) error
|
||||
}
|
||||
|
||||
|
||||
// FanUnit abstracts the fan unit
|
||||
type FanUnit interface {
|
||||
|
||||
// Kind returns the kind of the fan FanUnit
|
||||
Kind() FanUnitKind
|
||||
|
||||
// Run the client with event loop
|
||||
Run(context.Context) error
|
||||
|
||||
// SetFanSpeedPercent sets the fan speed in percent.
|
||||
SetFanSpeedPercent(context.Context, uint8) error
|
||||
|
||||
// SetLed sets the LED color. Noop if the LED is not available.
|
||||
SetLed(context.Context, led.Color) error
|
||||
|
||||
// FanSpeedRPM returns the current fan speed in rotations per minute.
|
||||
FanSpeedRPM(context.Context) (float64, error)
|
||||
|
||||
// WaitForButtonPress blocks until the button is pressed. Noop if the button is not available.
|
||||
WaitForButtonPress(context.Context) error
|
||||
|
||||
// AirFlowTemperature returns the temperature of the air flow. Noop if the sensor is not available.
|
||||
AirFlowTemperature(context.Context) (float32, error)
|
||||
|
||||
Close() error
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build linux
|
||||
//go:build linux && !tinygo
|
||||
|
||||
package hal
|
||||
|
||||
@@ -16,6 +16,10 @@ import (
|
||||
|
||||
"github.com/warthog618/gpiod"
|
||||
"github.com/warthog618/gpiod/device/rpi"
|
||||
"github.com/xvzf/computeblade-agent/pkg/hal/led"
|
||||
"github.com/xvzf/computeblade-agent/pkg/log"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -52,6 +56,8 @@ const (
|
||||
bcm2711DebounceInterval = 100 * time.Millisecond
|
||||
|
||||
bcm2711ThermalZonePath = "/sys/class/thermal/thermal_zone0/temp"
|
||||
|
||||
smartFanUnitDev = "/dev/ttyAMA5" // UART5
|
||||
)
|
||||
|
||||
type bcm2711 struct {
|
||||
@@ -73,7 +79,7 @@ type bcm2711 struct {
|
||||
gpioChip0 *gpiod.Chip
|
||||
|
||||
// Save LED colors so the pixels can be updated individually
|
||||
leds [2]LedColor
|
||||
leds [2]led.Color
|
||||
|
||||
// Stealth mode output
|
||||
stealthModeLine *gpiod.Line
|
||||
@@ -86,13 +92,11 @@ type bcm2711 struct {
|
||||
// PoE detection input
|
||||
poeLine *gpiod.Line
|
||||
|
||||
// Fan tach input
|
||||
fanEdgeLine *gpiod.Line
|
||||
lastFanEdgeEvent *gpiod.LineEvent
|
||||
fanRpm float64
|
||||
// Fan unit
|
||||
fanUnit FanUnit
|
||||
}
|
||||
|
||||
func NewCm4Hal(opts ComputeBladeHalOpts) (ComputeBladeHal, error) {
|
||||
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 {
|
||||
@@ -134,83 +138,32 @@ func NewCm4Hal(opts ComputeBladeHalOpts) (ComputeBladeHal, error) {
|
||||
|
||||
computeModule.WithLabelValues("cm4").Set(1)
|
||||
|
||||
return bcm, bcm.setup()
|
||||
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.edgeButtonLine.Close(),
|
||||
bcm.poeLine.Close(),
|
||||
bcm.stealthModeLine.Close(),
|
||||
)
|
||||
|
||||
if bcm.fanEdgeLine != nil {
|
||||
return errors.Join(errs, bcm.fanEdgeLine.Close())
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// handleFanEdge handles an edge event on the fan tach input for the standard fan unite.
|
||||
// Exponential moving average is used to smooth out the fan speed.
|
||||
func (bcm *bcm2711) handleFanEdge(evt gpiod.LineEvent) {
|
||||
// Ensure we're always storing the last event
|
||||
defer func() {
|
||||
bcm.lastFanEdgeEvent = &evt
|
||||
}()
|
||||
|
||||
// First event, we cannot extrapolate the fan speed yet
|
||||
if bcm.lastFanEdgeEvent == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate time delta between events
|
||||
delta := evt.Timestamp - bcm.lastFanEdgeEvent.Timestamp
|
||||
ticksPerSecond := 1000.0 / float64(delta.Milliseconds())
|
||||
rpm := (ticksPerSecond * 60.0) / 2.0 // 2 ticks per revolution
|
||||
|
||||
// Simple moving average to smooth out the fan speed
|
||||
bcm.fanRpm = (rpm * 0.1) + (bcm.fanRpm * 0.9)
|
||||
fanSpeed.Set(bcm.fanRpm)
|
||||
}
|
||||
|
||||
func (bcm *bcm2711) handleEdgeButtonEdge(evt gpiod.LineEvent) {
|
||||
// Despite the debounce, 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(ctx context.Context) error {
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// Init initialises GPIOs and sets sane defaults
|
||||
func (bcm *bcm2711) setup() error {
|
||||
func (bcm *bcm2711) setup(ctx context.Context) error {
|
||||
var err error = nil
|
||||
|
||||
// Register edge event handler for edge button
|
||||
@@ -233,29 +186,97 @@ func (bcm *bcm2711) setup() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// standard fan unit
|
||||
if bcm.opts.FanUnit == FanUnitStandard {
|
||||
fanUnit.WithLabelValues("standard").Set(1)
|
||||
// FAN PWM output for standard fan unit (GPIO 12)
|
||||
// -> bcm2711RegGpfsel1 8:6, alt0
|
||||
bcm.gpioMem[bcm2711RegGpfsel1] = (bcm.gpioMem[bcm2711RegGpfsel1] &^ (0b111 << 6)) | (0b100 << 6)
|
||||
// Register edge event handler for fan tach input
|
||||
bcm.fanEdgeLine, err = bcm.gpioChip0.RequestLine(
|
||||
rpi.GPIO13,
|
||||
gpiod.WithEventHandler(bcm.handleFanEdge),
|
||||
gpiod.WithFallingEdge,
|
||||
gpiod.WithPullUp,
|
||||
)
|
||||
// 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).Error("detected smart fan unit")
|
||||
bcm.fanUnit, err = NewSmartFanUnit(smartFanUnitDev)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
log.FromContext(ctx).Info("no smart fan unit detected, assuming standard fan unit", zap.Error(err))
|
||||
// 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 err
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bcm2711 *bcm2711) GetFanRPM() (float64, error) {
|
||||
return bcm2711.fanRpm, 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 the debounce, 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).Error("failed to wait for button press", zap.Error(err))
|
||||
} 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) {
|
||||
@@ -316,9 +337,7 @@ func (bcm *bcm2711) setPwm0Freq(targetFrequency uint64) error {
|
||||
|
||||
// SetFanSpeed sets the fanspeed of a blade in percent (standard fan unit)
|
||||
func (bcm *bcm2711) SetFanSpeed(speed uint8) error {
|
||||
fanSpeedTargetPercent.Set(float64(speed))
|
||||
bcm.setFanSpeedPWM(speed)
|
||||
return nil
|
||||
return bcm.fanUnit.SetFanSpeedPercent(context.TODO(), speed)
|
||||
}
|
||||
|
||||
func (bcm *bcm2711) setFanSpeedPWM(speed uint8) {
|
||||
@@ -381,11 +400,16 @@ func serializePwmDataFrame(data uint8) uint32 {
|
||||
return result
|
||||
}
|
||||
|
||||
func (bcm *bcm2711) SetLed(idx uint, color LedColor) error {
|
||||
func (bcm *bcm2711) SetLed(idx uint, 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 {
|
||||
bcm.fanUnit.SetLed(context.TODO(), color)
|
||||
}
|
||||
|
||||
bcm.leds[idx] = color
|
||||
|
||||
return bcm.updateLEDs()
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/xvzf/computeblade-agent/pkg/hal/led"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
@@ -17,18 +18,25 @@ type SimulatedHal struct {
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func NewCm4Hal(_ ComputeBladeHalOpts) (ComputeBladeHal, error) {
|
||||
func NewCm4Hal(_ context.Context, _ ComputeBladeHalOpts) (ComputeBladeHal, error) {
|
||||
logger := zap.L().Named("hal").Named("simulated-cm4")
|
||||
logger.Warn("Using simulated hal")
|
||||
|
||||
computeModule.WithLabelValues("simulated").Set(1)
|
||||
fanUnit.WithLabelValues("simulated").Set(1)
|
||||
|
||||
socTemperature.Set(42)
|
||||
|
||||
return &SimulatedHal{
|
||||
logger: logger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *SimulatedHal) Run(ctx context.Context) error {
|
||||
<-ctx.Done()
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
func (m *SimulatedHal) Close() error {
|
||||
return nil
|
||||
}
|
||||
@@ -71,7 +79,7 @@ func (m *SimulatedHal) WaitForEdgeButtonPress(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *SimulatedHal) SetLed(idx uint, color LedColor) error {
|
||||
func (m *SimulatedHal) SetLed(idx uint, color led.Color) error {
|
||||
ledColorChangeEventCount.Inc()
|
||||
m.logger.Info("SetLed", zap.Uint("idx", idx), zap.Any("color", color))
|
||||
return nil
|
||||
|
||||
100
pkg/hal/hal_bcm2711_standardfanunit.go
Normal file
100
pkg/hal/hal_bcm2711_standardfanunit.go
Normal file
@@ -0,0 +1,100 @@
|
||||
//go:build linux && !tinygo
|
||||
|
||||
package hal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
|
||||
"github.com/warthog618/gpiod"
|
||||
"github.com/warthog618/gpiod/device/rpi"
|
||||
"github.com/xvzf/computeblade-agent/pkg/hal/led"
|
||||
)
|
||||
|
||||
type standardFanUnitBcm2711 struct {
|
||||
GpioChip0 *gpiod.Chip
|
||||
SetFanSpeedPwmFunc func(speed uint8) error
|
||||
DisableRPMreporting bool
|
||||
|
||||
// Fan tach input
|
||||
fanEdgeLine *gpiod.Line
|
||||
lastFanEdgeEvent *gpiod.LineEvent
|
||||
fanRpm float64
|
||||
}
|
||||
|
||||
func (fu standardFanUnitBcm2711) Kind() FanUnitKind {
|
||||
if fu.DisableRPMreporting {
|
||||
return FanUnitKindStandardNoRPM
|
||||
}
|
||||
return FanUnitKindStandard
|
||||
}
|
||||
|
||||
func (fu standardFanUnitBcm2711) Run(ctx context.Context) error {
|
||||
var err error
|
||||
fanUnit.WithLabelValues("standard").Set(1)
|
||||
|
||||
// Register edge event handler for fan tach input
|
||||
if !fu.DisableRPMreporting {
|
||||
fu.fanEdgeLine, err = fu.GpioChip0.RequestLine(
|
||||
rpi.GPIO13,
|
||||
gpiod.WithEventHandler(fu.handleFanEdge),
|
||||
gpiod.WithFallingEdge,
|
||||
gpiod.WithPullUp,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fu.fanEdgeLine.Close()
|
||||
}
|
||||
|
||||
<-ctx.Done()
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
// handleFanEdge handles an edge event on the fan tach input for the standard fan unite.
|
||||
// Exponential moving average is used to smooth out the fan speed.
|
||||
func (fu *standardFanUnitBcm2711) handleFanEdge(evt gpiod.LineEvent) {
|
||||
// Ensure we're always storing the last event
|
||||
defer func() {
|
||||
fu.lastFanEdgeEvent = &evt
|
||||
}()
|
||||
|
||||
// First event, we cannot extrapolate the fan speed yet
|
||||
if fu.lastFanEdgeEvent == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate time delta between events
|
||||
delta := evt.Timestamp - fu.lastFanEdgeEvent.Timestamp
|
||||
ticksPerSecond := 1000.0 / float64(delta.Milliseconds())
|
||||
rpm := (ticksPerSecond * 60.0) / 2.0 // 2 ticks per revolution
|
||||
|
||||
// Simple moving average to smooth out the fan speed
|
||||
fu.fanRpm = (rpm * 0.1) + (fu.fanRpm * 0.9)
|
||||
fanSpeed.Set(fu.fanRpm)
|
||||
}
|
||||
|
||||
func (fu *standardFanUnitBcm2711) SetFanSpeedPercent(_ context.Context, percent uint8) error {
|
||||
return fu.SetFanSpeedPwmFunc(percent)
|
||||
}
|
||||
|
||||
func (fu *standardFanUnitBcm2711) SetLed(_ context.Context, _ led.Color) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fu *standardFanUnitBcm2711) FanSpeedRPM(_ context.Context) (float64, error) {
|
||||
return fu.fanRpm, nil
|
||||
}
|
||||
|
||||
func (fu *standardFanUnitBcm2711) WaitForButtonPress(ctx context.Context) error {
|
||||
<-ctx.Done()
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
func (fu *standardFanUnitBcm2711) AirFlowTemperature(_ context.Context) (float32, error) {
|
||||
return -1 * math.MaxFloat32, nil
|
||||
}
|
||||
|
||||
func (fu *standardFanUnitBcm2711) Close() error {
|
||||
return nil
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/xvzf/computeblade-agent/pkg/hal/led"
|
||||
)
|
||||
|
||||
// fails if ComputeBladeHalMock does not implement ComputeBladeHal
|
||||
@@ -14,6 +15,11 @@ type ComputeBladeHalMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *ComputeBladeHalMock) Run(ctx context.Context) error {
|
||||
args := m.Called(ctx)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *ComputeBladeHalMock) Close() error {
|
||||
args := m.Called()
|
||||
return args.Error(0)
|
||||
@@ -44,7 +50,7 @@ func (m *ComputeBladeHalMock) WaitForEdgeButtonPress(ctx context.Context) error
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *ComputeBladeHalMock) SetLed(idx uint, color LedColor) error {
|
||||
func (m *ComputeBladeHalMock) SetLed(idx uint, color led.Color) error {
|
||||
args := m.Called(idx, color)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
7
pkg/hal/led/types.go
Normal file
7
pkg/hal/led/types.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package led
|
||||
|
||||
type Color struct {
|
||||
Red uint8 `mapstructure:"red"`
|
||||
Green uint8 `mapstructure:"green"`
|
||||
Blue uint8 `mapstructure:"blue"`
|
||||
}
|
||||
60
pkg/hal/metrics.go
Normal file
60
pkg/hal/metrics.go
Normal file
@@ -0,0 +1,60 @@
|
||||
//go:build !tinygo
|
||||
package hal
|
||||
|
||||
import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
|
||||
var (
|
||||
fanSpeedTargetPercent = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: "computeblade",
|
||||
Name: "fan_speed_target_percent",
|
||||
Help: "Target fanspeed in percent",
|
||||
})
|
||||
fanSpeed = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: "computeblade",
|
||||
Name: "fan_speed",
|
||||
Help: "Fan speed in RPM",
|
||||
})
|
||||
socTemperature = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: "computeblade",
|
||||
Name: "soc_temperature",
|
||||
Help: "SoC temperature in °C",
|
||||
})
|
||||
airFlowTemperature = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: "computeblade",
|
||||
Name: "airflow_temperature",
|
||||
Help: "airflow temperature in °C",
|
||||
})
|
||||
computeModule = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: "computeblade",
|
||||
Name: "compute_modul_present",
|
||||
Help: "Compute module type",
|
||||
}, []string{"type"})
|
||||
ledColorChangeEventCount = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Namespace: "computeblade",
|
||||
Name: "led_color_change_event_count",
|
||||
Help: "Led color change event_count",
|
||||
})
|
||||
powerStatus = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: "computeblade",
|
||||
Name: "power_status",
|
||||
Help: "Power status of the blade",
|
||||
}, []string{"type"})
|
||||
stealthModeEnabled = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: "computeblade",
|
||||
Name: "stealth_mode_enabled",
|
||||
Help: "Stealth mode enabled",
|
||||
})
|
||||
fanUnit = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: "computeblade",
|
||||
Name: "fan_unit",
|
||||
Help: "Fan unit",
|
||||
}, []string{"type"})
|
||||
edgeButtonEventCount = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Namespace: "computeblade",
|
||||
Name: "edge_button_event_count",
|
||||
Help: "Number of edge button presses",
|
||||
})
|
||||
)
|
||||
204
pkg/hal/smartfanunit.go
Normal file
204
pkg/hal/smartfanunit.go
Normal file
@@ -0,0 +1,204 @@
|
||||
//go:build !tinygo
|
||||
|
||||
package hal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/xvzf/computeblade-agent/pkg/eventbus"
|
||||
"github.com/xvzf/computeblade-agent/pkg/hal/led"
|
||||
"github.com/xvzf/computeblade-agent/pkg/log"
|
||||
"github.com/xvzf/computeblade-agent/pkg/smartfanunit"
|
||||
"github.com/xvzf/computeblade-agent/pkg/smartfanunit/proto"
|
||||
"go.bug.st/serial"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
func SmartFanUnitPresent(ctx context.Context, portName string) (bool, error) {
|
||||
// Open the serial port.
|
||||
log.FromContext(ctx).Warn("Opening serial port")
|
||||
|
||||
rwc, err := serial.Open(portName, &serial.Mode{
|
||||
BaudRate: smartfanunit.Baudrate,
|
||||
})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
log.FromContext(ctx).Warn("Opened serial port")
|
||||
defer rwc.Close()
|
||||
|
||||
// Close reader after context is done
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
log.FromContext(ctx).Warn("Closing serial port")
|
||||
rwc.Close()
|
||||
}()
|
||||
|
||||
// read byte after byte, matching it to the SOF header used by the smart fan unit protocol.
|
||||
// -> if that's present, we have a smart fanunit connected.
|
||||
for {
|
||||
b := make([]byte, 1)
|
||||
log.FromContext(ctx).Info("Waiting for next byte from serial port")
|
||||
_, err := rwc.Read(b)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if b[0] == proto.SOF {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func NewSmartFanUnit(portName string) (FanUnit, error) {
|
||||
// Open the serial port.
|
||||
rwc, err := serial.Open(portName, &serial.Mode{
|
||||
BaudRate: smartfanunit.Baudrate,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &smartFanUnit{
|
||||
rwc: rwc,
|
||||
eb: eventbus.New(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
var ErrCommunicationFailed = errors.New("communication failed")
|
||||
|
||||
const (
|
||||
inboundTopic = "smartfanunit:inbound"
|
||||
outboundTopic = "smartfanunit:outbound"
|
||||
)
|
||||
|
||||
type smartFanUnit struct {
|
||||
rwc io.ReadWriteCloser
|
||||
mu sync.Mutex // write mutex
|
||||
|
||||
speed smartfanunit.FanSpeedRPMPacket
|
||||
airflow smartfanunit.AirFlowTemperaturePacket
|
||||
|
||||
eb eventbus.EventBus
|
||||
}
|
||||
|
||||
func (fuc *smartFanUnit) Kind() FanUnitKind {
|
||||
return FanUnitKindSmart
|
||||
}
|
||||
|
||||
// Run the client with event loop
|
||||
func (fuc *smartFanUnit) Run(parentCtx context.Context) error {
|
||||
fanUnit.WithLabelValues("smart").Set(1)
|
||||
|
||||
ctx, cancel := context.WithCancelCause(parentCtx)
|
||||
defer cancel(nil)
|
||||
|
||||
wg := errgroup.Group{}
|
||||
|
||||
// Start read loop
|
||||
wg.Go(func() error {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
|
||||
pkt, err := proto.ReadPacket(ctx, fuc.rwc)
|
||||
if err != nil {
|
||||
log.FromContext(ctx).Error("Failed to read packet from serial port", zap.Error(err))
|
||||
continue
|
||||
}
|
||||
fuc.eb.Publish(inboundTopic, pkt)
|
||||
}
|
||||
})
|
||||
|
||||
// Subscribe to fan speed updates
|
||||
wg.Go(func() error {
|
||||
sub := fuc.eb.Subscribe(inboundTopic, 1, smartfanunit.MatchCmd(smartfanunit.NotifyFanSpeedRPM))
|
||||
defer sub.Unsubscribe()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case pktAny := <-sub.C():
|
||||
rawPkt := pktAny.(proto.Packet)
|
||||
if err := fuc.speed.FromPacket(rawPkt); err != nil && err != proto.ErrChecksumMismatch {
|
||||
return err
|
||||
}
|
||||
fanSpeed.Set(float64(fuc.speed.RPM))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Subscribe to air flow temperature updates
|
||||
wg.Go(func() error {
|
||||
sub := fuc.eb.Subscribe(inboundTopic, 1, smartfanunit.MatchCmd(smartfanunit.NotifyAirFlowTemperature))
|
||||
defer sub.Unsubscribe()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case pktAny := <-sub.C():
|
||||
rawPkt := pktAny.(proto.Packet)
|
||||
if err := fuc.airflow.FromPacket(rawPkt); err != nil && err != proto.ErrChecksumMismatch {
|
||||
return err
|
||||
}
|
||||
airFlowTemperature.Set(float64(fuc.airflow.Temperature))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return wg.Wait()
|
||||
}
|
||||
|
||||
func (fuc *smartFanUnit) write(ctx context.Context, pktGen smartfanunit.PacketGenerator) error {
|
||||
fuc.mu.Lock()
|
||||
defer fuc.mu.Unlock()
|
||||
return proto.WritePacket(ctx, fuc.rwc, pktGen.Packet())
|
||||
}
|
||||
|
||||
// SetFanSpeedPercent sets the fan speed in percent.
|
||||
func (fuc *smartFanUnit) SetFanSpeedPercent(ctx context.Context, percent uint8) error {
|
||||
return fuc.write(ctx, &smartfanunit.SetFanSpeedPercentPacket{Percent: percent})
|
||||
}
|
||||
|
||||
// SetLed sets the LED color.
|
||||
func (fuc *smartFanUnit) SetLed(ctx context.Context, color led.Color) error {
|
||||
return fuc.write(ctx, &smartfanunit.SetLEDPacket{Color: color})
|
||||
}
|
||||
|
||||
// FanSpeedRPM returns the current fan speed in rotations per minute.
|
||||
func (fuc *smartFanUnit) FanSpeedRPM(_ context.Context) (float64, error) {
|
||||
return float64(fuc.speed.RPM), nil
|
||||
}
|
||||
|
||||
// WaitForButtonPress blocks until the button is pressed.
|
||||
func (fuc *smartFanUnit) WaitForButtonPress(ctx context.Context) error {
|
||||
sub := fuc.eb.Subscribe(inboundTopic, 1, smartfanunit.MatchCmd(smartfanunit.NotifyButtonPress))
|
||||
defer sub.Unsubscribe()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case pktAny := <-sub.C():
|
||||
rawPkt := pktAny.(proto.Packet)
|
||||
if rawPkt.Command != smartfanunit.NotifyButtonPress {
|
||||
return errors.New("unexpected packet")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AirFlowTemperature returns the temperature of the air flow.
|
||||
func (fuc *smartFanUnit) AirFlowTemperature(_ context.Context) (float32, error) {
|
||||
return fuc.airflow.Temperature, nil
|
||||
}
|
||||
|
||||
func (fuc *smartFanUnit) Close() error {
|
||||
return fuc.rwc.Close()
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/xvzf/computeblade-agent/pkg/hal"
|
||||
"github.com/xvzf/computeblade-agent/pkg/hal/led"
|
||||
"github.com/xvzf/computeblade-agent/pkg/util"
|
||||
)
|
||||
|
||||
@@ -28,9 +29,9 @@ type ledEngineImpl struct {
|
||||
|
||||
type BlinkPattern struct {
|
||||
// BaseColor is the color is the color shown when the pattern starts (-> before the first blink)
|
||||
BaseColor hal.LedColor
|
||||
BaseColor led.Color
|
||||
// ActiveColor is the color shown when the pattern is active (-> during the blink)
|
||||
ActiveColor hal.LedColor
|
||||
ActiveColor led.Color
|
||||
// Delays is a list of delays between changes -> (base) -> 0.5s(active) -> 1s(base) -> 0.5s (active) -> 1s (base)
|
||||
Delays []time.Duration
|
||||
}
|
||||
@@ -39,24 +40,24 @@ func mapBrighnessUint8(brightness float64) uint8 {
|
||||
return uint8(255.0 * brightness)
|
||||
}
|
||||
|
||||
func LedColorPurple(brightness float64) hal.LedColor {
|
||||
return hal.LedColor{
|
||||
func LedColorPurple(brightness float64) led.Color {
|
||||
return led.Color{
|
||||
Red: mapBrighnessUint8(brightness),
|
||||
Green: 0,
|
||||
Blue: mapBrighnessUint8(brightness),
|
||||
}
|
||||
}
|
||||
|
||||
func LedColorRed(brightness float64) hal.LedColor {
|
||||
return hal.LedColor{
|
||||
func LedColorRed(brightness float64) led.Color {
|
||||
return led.Color{
|
||||
Red: mapBrighnessUint8(brightness),
|
||||
Green: 0,
|
||||
Blue: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func LedColorGreen(brightness float64) hal.LedColor {
|
||||
return hal.LedColor{
|
||||
func LedColorGreen(brightness float64) led.Color {
|
||||
return led.Color{
|
||||
Red: 0,
|
||||
Green: mapBrighnessUint8(brightness),
|
||||
Blue: 0,
|
||||
@@ -64,7 +65,7 @@ func LedColorGreen(brightness float64) hal.LedColor {
|
||||
}
|
||||
|
||||
// NewStaticPattern creates a new static pattern (no color changes)
|
||||
func NewStaticPattern(color hal.LedColor) BlinkPattern {
|
||||
func NewStaticPattern(color led.Color) BlinkPattern {
|
||||
return BlinkPattern{
|
||||
BaseColor: color,
|
||||
ActiveColor: color,
|
||||
@@ -73,23 +74,23 @@ func NewStaticPattern(color hal.LedColor) BlinkPattern {
|
||||
}
|
||||
|
||||
// NewBurstPattern creates a new burst pattern (~1s cycle duration with 3x 50ms bursts)
|
||||
func NewBurstPattern(baseColor hal.LedColor, burstColor hal.LedColor) BlinkPattern {
|
||||
func NewBurstPattern(baseColor led.Color, burstColor led.Color) BlinkPattern {
|
||||
return BlinkPattern{
|
||||
BaseColor: baseColor,
|
||||
ActiveColor: burstColor,
|
||||
Delays: []time.Duration{
|
||||
750 * time.Millisecond, // 750ms off
|
||||
50 * time.Millisecond, // 50ms on
|
||||
50 * time.Millisecond, // 50ms off
|
||||
50 * time.Millisecond, // 50ms on
|
||||
50 * time.Millisecond, // 50ms off
|
||||
50 * time.Millisecond, // 50ms on
|
||||
500 * time.Millisecond, // 750ms off
|
||||
100 * time.Millisecond, // 100ms on
|
||||
100 * time.Millisecond, // 100ms off
|
||||
100 * time.Millisecond, // 100ms on
|
||||
100 * time.Millisecond, // 100ms off
|
||||
100 * time.Millisecond, // 100ms on
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewSlowBlinkPattern creates a new slow blink pattern (~2s cycle duration with 1s off and 1s on)
|
||||
func NewSlowBlinkPattern(baseColor hal.LedColor, activeColor hal.LedColor) BlinkPattern {
|
||||
func NewSlowBlinkPattern(baseColor led.Color, activeColor led.Color) BlinkPattern {
|
||||
return BlinkPattern{
|
||||
BaseColor: baseColor,
|
||||
ActiveColor: activeColor,
|
||||
@@ -118,8 +119,8 @@ func NewLedEngine(opts LedEngineOpts) *ledEngineImpl {
|
||||
return &ledEngineImpl{
|
||||
ledIdx: opts.LedIdx,
|
||||
hal: opts.Hal,
|
||||
restart: make(chan struct{}), // restart channel controls cancelation of any pattern
|
||||
pattern: NewStaticPattern(hal.LedColor{}), // Turn off LEDs by default
|
||||
restart: make(chan struct{}), // restart channel controls cancelation of any pattern
|
||||
pattern: NewStaticPattern(led.Color{}), // Turn off LEDs by default
|
||||
clock: clock,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/xvzf/computeblade-agent/pkg/hal"
|
||||
"github.com/xvzf/computeblade-agent/pkg/hal/led"
|
||||
"github.com/xvzf/computeblade-agent/pkg/ledengine"
|
||||
"github.com/xvzf/computeblade-agent/pkg/util"
|
||||
)
|
||||
@@ -18,7 +19,7 @@ func TestNewStaticPattern(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type args struct {
|
||||
color hal.LedColor
|
||||
color led.Color
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -27,19 +28,19 @@ func TestNewStaticPattern(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
"Green",
|
||||
args{hal.LedColor{Green: 255}},
|
||||
args{led.Color{Green: 255}},
|
||||
ledengine.BlinkPattern{
|
||||
BaseColor: hal.LedColor{Green: 255},
|
||||
ActiveColor: hal.LedColor{Green: 255},
|
||||
BaseColor: led.Color{Green: 255},
|
||||
ActiveColor: led.Color{Green: 255},
|
||||
Delays: []time.Duration{time.Hour},
|
||||
},
|
||||
},
|
||||
{
|
||||
"Red",
|
||||
args{hal.LedColor{Red: 255}},
|
||||
args{led.Color{Red: 255}},
|
||||
ledengine.BlinkPattern{
|
||||
BaseColor: hal.LedColor{Red: 255},
|
||||
ActiveColor: hal.LedColor{Red: 255},
|
||||
BaseColor: led.Color{Red: 255},
|
||||
ActiveColor: led.Color{Red: 255},
|
||||
Delays: []time.Duration{time.Hour},
|
||||
},
|
||||
},
|
||||
@@ -56,8 +57,8 @@ func TestNewStaticPattern(t *testing.T) {
|
||||
func TestNewBurstPattern(t *testing.T) {
|
||||
t.Parallel()
|
||||
type args struct {
|
||||
baseColor hal.LedColor
|
||||
burstColor hal.LedColor
|
||||
baseColor led.Color
|
||||
burstColor led.Color
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -67,38 +68,38 @@ func TestNewBurstPattern(t *testing.T) {
|
||||
{
|
||||
"Green <-> Red",
|
||||
args{
|
||||
baseColor: hal.LedColor{Green: 255},
|
||||
burstColor: hal.LedColor{Red: 255},
|
||||
baseColor: led.Color{Green: 255},
|
||||
burstColor: led.Color{Red: 255},
|
||||
},
|
||||
ledengine.BlinkPattern{
|
||||
BaseColor: hal.LedColor{Green: 255},
|
||||
ActiveColor: hal.LedColor{Red: 255},
|
||||
BaseColor: led.Color{Green: 255},
|
||||
ActiveColor: led.Color{Red: 255},
|
||||
Delays: []time.Duration{
|
||||
750 * time.Millisecond,
|
||||
50 * time.Millisecond,
|
||||
50 * time.Millisecond,
|
||||
50 * time.Millisecond,
|
||||
50 * time.Millisecond,
|
||||
50 * time.Millisecond,
|
||||
500 * time.Millisecond, // 750ms off
|
||||
100 * time.Millisecond, // 100ms on
|
||||
100 * time.Millisecond, // 100ms off
|
||||
100 * time.Millisecond, // 100ms on
|
||||
100 * time.Millisecond, // 100ms off
|
||||
100 * time.Millisecond, // 100ms on
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"Green <-> Green (valid, but no visual effect)",
|
||||
args{
|
||||
baseColor: hal.LedColor{Green: 255},
|
||||
burstColor: hal.LedColor{Green: 255},
|
||||
baseColor: led.Color{Green: 255},
|
||||
burstColor: led.Color{Green: 255},
|
||||
},
|
||||
ledengine.BlinkPattern{
|
||||
BaseColor: hal.LedColor{Green: 255},
|
||||
ActiveColor: hal.LedColor{Green: 255},
|
||||
BaseColor: led.Color{Green: 255},
|
||||
ActiveColor: led.Color{Green: 255},
|
||||
Delays: []time.Duration{
|
||||
750 * time.Millisecond,
|
||||
50 * time.Millisecond,
|
||||
50 * time.Millisecond,
|
||||
50 * time.Millisecond,
|
||||
50 * time.Millisecond,
|
||||
50 * time.Millisecond,
|
||||
500 * time.Millisecond, // 750ms off
|
||||
100 * time.Millisecond, // 100ms on
|
||||
100 * time.Millisecond, // 100ms off
|
||||
100 * time.Millisecond, // 100ms on
|
||||
100 * time.Millisecond, // 100ms off
|
||||
100 * time.Millisecond, // 100ms on
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -114,8 +115,8 @@ func TestNewBurstPattern(t *testing.T) {
|
||||
|
||||
func TestNewSlowBlinkPattern(t *testing.T) {
|
||||
type args struct {
|
||||
baseColor hal.LedColor
|
||||
activeColor hal.LedColor
|
||||
baseColor led.Color
|
||||
activeColor led.Color
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -154,8 +155,8 @@ func Test_LedEngine_SetPattern_WhileRunning(t *testing.T) {
|
||||
clk.On("After", time.Hour).Times(2).Return(clkAfterChan)
|
||||
|
||||
cbMock := hal.ComputeBladeHalMock{}
|
||||
cbMock.On("SetLed", uint(0), hal.LedColor{Green: 0, Blue: 0, Red: 0}).Once().Return(nil)
|
||||
cbMock.On("SetLed", uint(0), hal.LedColor{Green: 0, Blue: 0, Red: 255}).Once().Return(nil)
|
||||
cbMock.On("SetLed", uint(0), led.Color{Green: 0, Blue: 0, Red: 0}).Once().Return(nil)
|
||||
cbMock.On("SetLed", uint(0), led.Color{Green: 0, Blue: 0, Red: 255}).Once().Return(nil)
|
||||
|
||||
opts := ledengine.LedEngineOpts{
|
||||
Hal: &cbMock,
|
||||
@@ -182,7 +183,7 @@ func Test_LedEngine_SetPattern_WhileRunning(t *testing.T) {
|
||||
|
||||
// Set pattern
|
||||
t.Log("Setting pattern")
|
||||
err := engine.SetPattern(ledengine.NewStaticPattern(hal.LedColor{Red: 255}))
|
||||
err := engine.SetPattern(ledengine.NewStaticPattern(led.Color{Red: 255}))
|
||||
assert.NoError(t, err)
|
||||
|
||||
t.Log("Canceling context")
|
||||
@@ -201,7 +202,7 @@ func Test_LedEngine_SetPattern_BeforeRun(t *testing.T) {
|
||||
clk.On("After", time.Hour).Once().Return(clkAfterChan)
|
||||
|
||||
cbMock := hal.ComputeBladeHalMock{}
|
||||
cbMock.On("SetLed", uint(0), hal.LedColor{Green: 0, Blue: 0, Red: 255}).Once().Return(nil)
|
||||
cbMock.On("SetLed", uint(0), led.Color{Green: 0, Blue: 0, Red: 255}).Once().Return(nil)
|
||||
|
||||
opts := ledengine.LedEngineOpts{
|
||||
Hal: &cbMock,
|
||||
@@ -212,7 +213,7 @@ func Test_LedEngine_SetPattern_BeforeRun(t *testing.T) {
|
||||
engine := ledengine.NewLedEngine(opts)
|
||||
// We want to change the pattern BEFORE the engine is started
|
||||
t.Log("Setting pattern")
|
||||
err := engine.SetPattern(ledengine.NewStaticPattern(hal.LedColor{Red: 255}))
|
||||
err := engine.SetPattern(ledengine.NewStaticPattern(led.Color{Red: 255}))
|
||||
assert.NoError(t, err)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
|
||||
@@ -243,8 +244,8 @@ func Test_LedEngine_SetPattern_SetLedFailureInPattern(t *testing.T) {
|
||||
clk.On("After", time.Hour).Once().Return(clkAfterChan)
|
||||
|
||||
cbMock := hal.ComputeBladeHalMock{}
|
||||
call0 := cbMock.On("SetLed", uint(0), hal.LedColor{Green: 0, Blue: 0, Red: 0}).Once().Return(nil)
|
||||
cbMock.On("SetLed", uint(0), hal.LedColor{Green: 0, Blue: 0, Red: 0}).Once().Return(errors.New("failure")).NotBefore(call0)
|
||||
call0 := cbMock.On("SetLed", uint(0), led.Color{Green: 0, Blue: 0, Red: 0}).Once().Return(nil)
|
||||
cbMock.On("SetLed", uint(0), led.Color{Green: 0, Blue: 0, Red: 0}).Once().Return(errors.New("failure")).NotBefore(call0)
|
||||
|
||||
opts := ledengine.LedEngineOpts{
|
||||
Hal: &cbMock,
|
||||
|
||||
125
pkg/smartfanunit/commands.go
Normal file
125
pkg/smartfanunit/commands.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package smartfanunit
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/xvzf/computeblade-agent/pkg/hal/led"
|
||||
"github.com/xvzf/computeblade-agent/pkg/smartfanunit/proto"
|
||||
)
|
||||
|
||||
const (
|
||||
// Blade -> FanUnit
|
||||
CmdSetFanSpeedPercent proto.Command = 0x01
|
||||
CmdSetLED proto.Command = 0x02
|
||||
|
||||
// FanUnit -> Blade, sent in regular intervals
|
||||
NotifyButtonPress proto.Command = 0xa1
|
||||
NotifyAirFlowTemperature proto.Command = 0xa2
|
||||
NotifyFanSpeedRPM proto.Command = 0xa3
|
||||
)
|
||||
|
||||
var ErrInvalidCommand = errors.New("invalid command")
|
||||
|
||||
type PacketGenerator interface {
|
||||
Packet() proto.Packet
|
||||
}
|
||||
|
||||
// SetFanSpeedPercentPacket is sent from the blade to the fan unit to set the fan speed in percent.
|
||||
type SetFanSpeedPercentPacket struct {
|
||||
Percent uint8
|
||||
}
|
||||
|
||||
func (p *SetFanSpeedPercentPacket) Packet() proto.Packet {
|
||||
return proto.Packet{
|
||||
Command: CmdSetFanSpeedPercent,
|
||||
Data: proto.Data{p.Percent, 0, 0},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *SetFanSpeedPercentPacket) FromPacket(packet proto.Packet) error {
|
||||
if packet.Command != CmdSetFanSpeedPercent {
|
||||
return ErrInvalidCommand
|
||||
}
|
||||
p.Percent = packet.Data[0]
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetLEDPacket is sent from the blade to the fan unit to set the LED color.
|
||||
type SetLEDPacket struct {
|
||||
Color led.Color
|
||||
}
|
||||
|
||||
func (p *SetLEDPacket) Packet() proto.Packet {
|
||||
return proto.Packet{
|
||||
Command: CmdSetLED,
|
||||
Data: proto.Data{p.Color.Blue, p.Color.Green, p.Color.Red},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *SetLEDPacket) FromPacket(packet proto.Packet) error {
|
||||
if packet.Command != CmdSetLED {
|
||||
return ErrInvalidCommand
|
||||
}
|
||||
p.Color = led.Color{
|
||||
Blue: packet.Data[0],
|
||||
Green: packet.Data[1],
|
||||
Red: packet.Data[2],
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ButtonPressPacket is sent from the fan unit to the blade when the button is pressed.
|
||||
type ButtonPressPacket struct{}
|
||||
|
||||
func (p *ButtonPressPacket) Packet() proto.Packet {
|
||||
return proto.Packet{
|
||||
Command: NotifyButtonPress,
|
||||
Data: proto.Data{},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ButtonPressPacket) FromPacket(packet proto.Packet) error {
|
||||
if packet.Command != NotifyButtonPress {
|
||||
return ErrInvalidCommand
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AirFlowTemperaturePacket is sent from the fan unit to the blade to report the current air flow temperature.
|
||||
type AirFlowTemperaturePacket struct {
|
||||
Temperature float32
|
||||
}
|
||||
|
||||
func (p *AirFlowTemperaturePacket) Packet() proto.Packet {
|
||||
return proto.Packet{
|
||||
Command: NotifyAirFlowTemperature,
|
||||
Data: proto.Data(float32To24Bit(p.Temperature)),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *AirFlowTemperaturePacket) FromPacket(packet proto.Packet) error {
|
||||
if packet.Command != NotifyAirFlowTemperature {
|
||||
return ErrInvalidCommand
|
||||
}
|
||||
p.Temperature = float32From24Bit(packet.Data)
|
||||
return nil
|
||||
}
|
||||
|
||||
// FanSpeedRPMPacket is sent from the fan unit to the blade to report the current fan speed in RPM.
|
||||
type FanSpeedRPMPacket struct {
|
||||
RPM float32
|
||||
}
|
||||
|
||||
func (p *FanSpeedRPMPacket) Packet() proto.Packet {
|
||||
return proto.Packet{
|
||||
Command: NotifyFanSpeedRPM,
|
||||
Data: float32To24Bit(p.RPM),
|
||||
}
|
||||
}
|
||||
func (p *FanSpeedRPMPacket) FromPacket(packet proto.Packet) error {
|
||||
if packet.Command != NotifyFanSpeedRPM {
|
||||
return ErrInvalidCommand
|
||||
}
|
||||
p.RPM = float32From24Bit(packet.Data)
|
||||
return nil
|
||||
}
|
||||
132
pkg/smartfanunit/emc2101/emc2101.go
Normal file
132
pkg/smartfanunit/emc2101/emc2101.go
Normal file
@@ -0,0 +1,132 @@
|
||||
// This is a driver for the EMC2101 fan controller
|
||||
// Based on https://ww1.microchip.com/downloads/en/DeviceDoc/2101.pdf
|
||||
package emc2101
|
||||
|
||||
import (
|
||||
"tinygo.org/x/drivers"
|
||||
)
|
||||
|
||||
type emc2101 struct {
|
||||
Address uint16
|
||||
bus drivers.I2C
|
||||
}
|
||||
|
||||
// EMC2101 is a driver for the EMC2101 fan controller
|
||||
type EMC2101 interface {
|
||||
// Init initializes the EMC2101
|
||||
Init() error
|
||||
// InternalTemperature returns the internal temperature of the EMC2101
|
||||
InternalTemperature() (float32, error)
|
||||
// ExternalTemperature returns the external temperature of the EMC2101
|
||||
ExternalTemperature() (float32, error)
|
||||
// SetFanPercent sets the fan speed as a percentage of max
|
||||
SetFanPercent(percent uint8) error
|
||||
// FanRPM returns the current fan speed in RPM
|
||||
FanRPM() (float32, error)
|
||||
}
|
||||
|
||||
const (
|
||||
// Address is the default I2C address for the EMC2101
|
||||
Address = 0x4C
|
||||
ConfigReg = 0x03
|
||||
FanConfigReg = 0x4a
|
||||
FanSpinUpReg = 0x4b
|
||||
FanSettingReg = 0x4c
|
||||
FanTachReadingLowReg = 0x46
|
||||
FanTachReadingHighReg = 0x47
|
||||
ExternalTempReg = 0x01
|
||||
InternalTempReg = 0x00
|
||||
)
|
||||
|
||||
func New(bus drivers.I2C) EMC2101 {
|
||||
return &emc2101{bus: bus, Address: Address}
|
||||
}
|
||||
|
||||
// updateReg updates a register with the given set and clear masks
|
||||
func (e *emc2101) updateReg(regAddr, setMask, clearMask uint8) error {
|
||||
buf := make([]uint8, 1)
|
||||
err := e.bus.Tx(e.Address, []byte{regAddr}, buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
toWrite := buf[0]
|
||||
toWrite |= setMask
|
||||
toWrite &= ^clearMask
|
||||
|
||||
if toWrite == buf[0] {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
return e.bus.Tx(e.Address, []byte{regAddr, toWrite}, nil)
|
||||
}
|
||||
|
||||
func (e *emc2101) Init() error {
|
||||
// set pwm mode
|
||||
// bit 4: 0 = PWM mode
|
||||
// bit 2: 1 = TACH input
|
||||
if err := e.updateReg(ConfigReg, (1 << 2), (1 << 4)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := e.updateReg(FanConfigReg, (1 << 5), 0); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
/*
|
||||
0x3 0b100
|
||||
0x4b 0b11111
|
||||
0x4a 0b100000
|
||||
0x4a 0b100000
|
||||
*/
|
||||
|
||||
// Configure fan spin up to ignore tach input
|
||||
// bit 5: 1 = Ignore tach input for spin up procedure
|
||||
if err := e.updateReg(FanSpinUpReg, 0, (1 << 5)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *emc2101) InternalTemperature() (float32, error) {
|
||||
buf := make([]byte, 1)
|
||||
if err := e.bus.Tx(e.Address, []byte{InternalTempReg}, buf); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return float32(buf[0]), nil
|
||||
}
|
||||
|
||||
func (e *emc2101) ExternalTemperature() (float32, error) {
|
||||
buf := make([]byte, 1)
|
||||
if err := e.bus.Tx(e.Address, []byte{ExternalTempReg}, buf); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return float32(buf[0]), nil
|
||||
}
|
||||
|
||||
func (e *emc2101) SetFanPercent(percent uint8) error {
|
||||
if percent > 100 {
|
||||
percent = 100
|
||||
}
|
||||
val := uint8(uint32(percent) * 63 / 100)
|
||||
return e.bus.Tx(e.Address, []byte{FanSettingReg, val}, nil)
|
||||
}
|
||||
|
||||
func (e *emc2101) FanRPM() (float32, error) {
|
||||
high := make([]byte, 1)
|
||||
low := make([]byte, 1)
|
||||
|
||||
err := e.bus.Tx(e.Address, []byte{FanTachReadingHighReg}, high)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
err = e.bus.Tx(e.Address, []byte{FanTachReadingLowReg}, low)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var tachCount int = int(high[0])<<8 | int(low[0])
|
||||
|
||||
return float32(5400000) / float32(tachCount), nil
|
||||
}
|
||||
21
pkg/smartfanunit/helpers.go
Normal file
21
pkg/smartfanunit/helpers.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package smartfanunit
|
||||
|
||||
import "github.com/xvzf/computeblade-agent/pkg/smartfanunit/proto"
|
||||
|
||||
func float32To24Bit(val float32) proto.Data {
|
||||
// Convert float32 to number with 3 bytes (0.1 precision)
|
||||
tmp := uint32(val * 10)
|
||||
if tmp > 0xffffff {
|
||||
tmp = 0xffffff // cap
|
||||
}
|
||||
return proto.Data{
|
||||
uint8((tmp >> 16) & 0xFF),
|
||||
uint8((tmp >> 8) & 0xFF),
|
||||
uint8(tmp & 0xFF),
|
||||
}
|
||||
}
|
||||
|
||||
func float32From24Bit(data proto.Data) float32 {
|
||||
tmp := uint32(data[0])<<16 | uint32(data[1])<<8 | uint32(data[2])
|
||||
return float32(tmp) / 10
|
||||
}
|
||||
34
pkg/smartfanunit/helpers_test.go
Normal file
34
pkg/smartfanunit/helpers_test.go
Normal file
@@ -0,0 +1,34 @@
|
||||
//go:build !tinygo
|
||||
package smartfanunit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFloat32ToAndFrom24Bit(t *testing.T) {
|
||||
tests := []struct {
|
||||
input float32
|
||||
expected float32
|
||||
}{
|
||||
{0.0, 0.0},
|
||||
{1.0, 1.0},
|
||||
{0.123, 0.1},
|
||||
{10.0, 10.0},
|
||||
{100.0, 100.0},
|
||||
{1677721.5, 1677721.5},
|
||||
{2000000.0, 1677721.5}, // Should be capped at 0xFFFFFF
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprintf("Input: %f", test.input), func(t *testing.T) {
|
||||
data := float32To24Bit(test.input)
|
||||
result := float32From24Bit(data)
|
||||
|
||||
// Check if the result is approximately equal within a small delta
|
||||
if result < test.expected-0.01 || result > test.expected+0.01 {
|
||||
t.Errorf("Expected %f, but got %f", test.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
148
pkg/smartfanunit/proto/proto.go
Normal file
148
pkg/smartfanunit/proto/proto.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package proto
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers"
|
||||
)
|
||||
|
||||
// Simple P2P protocol for communicating over a serial port.
|
||||
// All commands are 4 bytes long, the first byte is the command, the remaining bytes are data
|
||||
// This allows encoding of 256 commands, with a payload of 3 bytes each.
|
||||
// Includes SOF/EOF framing and a checksum. Colliding bytes in the payload are escaped.
|
||||
|
||||
var (
|
||||
ErrChecksumMismatch = errors.New("checksum mismatch")
|
||||
ErrInvalidFramingByte = errors.New("invalid framing byte")
|
||||
)
|
||||
|
||||
const (
|
||||
SOF = 0x7E // Start of Frame
|
||||
ESC = 0x7D // Escape character
|
||||
XOR = 0x20 // XOR value for escaping
|
||||
EOF = 0x7F // End of Frame
|
||||
)
|
||||
|
||||
// Command represents the command byte.
|
||||
type Command uint8
|
||||
|
||||
// Data represents the three data bytes.
|
||||
type Data [3]uint8
|
||||
|
||||
// Packet represents a serial packet with command and data.
|
||||
type Packet struct {
|
||||
Command Command
|
||||
Data Data
|
||||
}
|
||||
|
||||
// Checksum calculates the Checksum for a packet.
|
||||
func (packet *Packet) Checksum() uint8 {
|
||||
crc := uint8(0)
|
||||
crc ^= uint8(packet.Command)
|
||||
for _, d := range packet.Data {
|
||||
crc ^= d
|
||||
}
|
||||
return crc
|
||||
}
|
||||
|
||||
// WritePacket writes a packet to an io.Writer with escaping.
|
||||
func WritePacket(_ context.Context, w io.Writer, packet Packet) error {
|
||||
checksum := packet.Checksum()
|
||||
|
||||
buf := []uint8{uint8(packet.Command), packet.Data[0], packet.Data[1], packet.Data[2], checksum}
|
||||
|
||||
_, err := w.Write([]uint8{SOF})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, b := range buf {
|
||||
if b == SOF || b == EOF || b == ESC {
|
||||
_, err := w.Write([]uint8{ESC, b ^ XOR})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
_, err := w.Write([]uint8{b})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
_, err = w.Write([]uint8{EOF})
|
||||
return err
|
||||
}
|
||||
|
||||
// ReadPacket reads a packet from an io.Reader with escaping.
|
||||
// This is blocking and drops invalid bytes until a valid packet is received.
|
||||
func ReadPacket(ctx context.Context, r io.Reader) (Packet, error) {
|
||||
buffer := []uint8{}
|
||||
|
||||
started := false
|
||||
escaped := false
|
||||
|
||||
uart, isUart := r.(drivers.UART)
|
||||
|
||||
for {
|
||||
|
||||
// Check if context is done before reading
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return Packet{}, ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
if isUart && uart.Buffered() == 0 {
|
||||
// Allows TinyGo to switch to other goroutines
|
||||
time.Sleep(time.Millisecond)
|
||||
continue
|
||||
}
|
||||
|
||||
b := make([]uint8, 1)
|
||||
_, err := r.Read(b)
|
||||
if err != nil {
|
||||
return Packet{}, err
|
||||
}
|
||||
|
||||
if b[0] == SOF && !started {
|
||||
started = true
|
||||
} else if !started {
|
||||
continue
|
||||
}
|
||||
|
||||
if escaped {
|
||||
buffer = append(buffer, b[0]^XOR)
|
||||
escaped = false
|
||||
} else if b[0] == ESC {
|
||||
escaped = true
|
||||
} else {
|
||||
buffer = append(buffer, b[0])
|
||||
}
|
||||
|
||||
if b[0] == EOF && !escaped {
|
||||
if len(buffer) == 7 { // Packet size
|
||||
break
|
||||
} else {
|
||||
buffer = []uint8{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if buffer[0] != SOF || buffer[len(buffer)-1] != EOF {
|
||||
return Packet{}, ErrInvalidFramingByte
|
||||
}
|
||||
|
||||
command := Command(buffer[1])
|
||||
data := Data{buffer[2], buffer[3], buffer[4]}
|
||||
checksum := buffer[5]
|
||||
pkt := Packet{command, data}
|
||||
expectedChecksum := pkt.Checksum()
|
||||
|
||||
if checksum != expectedChecksum {
|
||||
return Packet{}, ErrChecksumMismatch
|
||||
}
|
||||
|
||||
return pkt, nil
|
||||
}
|
||||
206
pkg/smartfanunit/proto/proto_test.go
Normal file
206
pkg/smartfanunit/proto/proto_test.go
Normal file
@@ -0,0 +1,206 @@
|
||||
package proto_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/xvzf/computeblade-agent/pkg/smartfanunit/proto"
|
||||
)
|
||||
|
||||
func TestWritePacket(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testcases := []struct {
|
||||
name string
|
||||
packet proto.Packet
|
||||
expected []uint8
|
||||
}{
|
||||
{
|
||||
name: "Simple packet",
|
||||
packet: proto.Packet{
|
||||
Command: proto.Command(0x01),
|
||||
Data: proto.Data{0x11, 0x12, 0x13},
|
||||
},
|
||||
expected: []uint8{proto.SOF, 0x01, 0x11, 0x12, 0x13, 0x11, proto.EOF},
|
||||
},
|
||||
{
|
||||
name: "ESC in payload and checksum == ESC",
|
||||
packet: proto.Packet{
|
||||
Command: proto.Command(0x01),
|
||||
Data: proto.Data{proto.ESC, 0x12, 0x13},
|
||||
// Checksup: 0x7d -> proto.ESC as well
|
||||
},
|
||||
expected: []uint8{
|
||||
// Start of frame
|
||||
proto.SOF,
|
||||
0x01,
|
||||
// Escaped data
|
||||
proto.ESC,
|
||||
proto.XOR ^ proto.ESC,
|
||||
// continuing non-escaped data
|
||||
0x12, 0x13,
|
||||
// escape checksum
|
||||
proto.ESC,
|
||||
proto.XOR ^ proto.ESC,
|
||||
// end of frame
|
||||
proto.EOF,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "EOF, SOF and ESC in payload",
|
||||
packet: proto.Packet{
|
||||
// 0x01, 0x7e, 0x7f, 0x7d
|
||||
Command: proto.Command(0xff),
|
||||
Data: proto.Data{proto.SOF, proto.EOF, proto.ESC},
|
||||
// Checksup: 0x7d -> proto.ESC as well
|
||||
},
|
||||
expected: []uint8{
|
||||
// Start of frame
|
||||
proto.SOF,
|
||||
0xff,
|
||||
// Escaped SOF
|
||||
proto.ESC,
|
||||
proto.XOR ^ proto.SOF,
|
||||
// Escaped EOF
|
||||
proto.ESC,
|
||||
proto.XOR ^ proto.EOF,
|
||||
// Escaped ESC
|
||||
proto.ESC,
|
||||
proto.XOR ^ proto.ESC,
|
||||
// Checksum
|
||||
0x83,
|
||||
// end of frame
|
||||
proto.EOF,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tcl := range testcases {
|
||||
tc := tcl
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
var buffer bytes.Buffer
|
||||
err := proto.WritePacket(context.TODO(), &buffer, tc.packet)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tc.expected, buffer.Bytes())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func FuzzPacketReadWrite(f *testing.F) {
|
||||
f.Add(uint8(0x01), uint8(0x02), uint8(0x03), uint8(0x04))
|
||||
|
||||
// Fuzz function
|
||||
f.Fuzz(func(t *testing.T, cmd, d0, d1, d2 uint8) {
|
||||
pkt := proto.Packet{
|
||||
Command: proto.Command(cmd),
|
||||
Data: proto.Data([]uint8{d0, d1, d2}),
|
||||
}
|
||||
|
||||
var buffer bytes.Buffer
|
||||
err := proto.WritePacket(context.TODO(), &buffer, pkt)
|
||||
assert.NoError(t, err)
|
||||
|
||||
readPkt, err := proto.ReadPacket(context.TODO(), &buffer)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, pkt, readPkt)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPacketReadWrite(t *testing.T) {
|
||||
testcases := []struct {
|
||||
name string
|
||||
packet proto.Packet
|
||||
}{
|
||||
{
|
||||
name: "Simple packet",
|
||||
packet: proto.Packet{
|
||||
Command: proto.Command(0x01),
|
||||
Data: proto.Data{0x11, 0x12, 0x13},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "EOF, SOF and ESC in payload",
|
||||
packet: proto.Packet{
|
||||
Command: proto.Command(0xff),
|
||||
Data: proto.Data{proto.SOF, proto.EOF, proto.ESC},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tcl := range testcases {
|
||||
tc := tcl
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
var buffer bytes.Buffer
|
||||
err := proto.WritePacket(context.TODO(), &buffer, tc.packet)
|
||||
assert.NoError(t, err)
|
||||
|
||||
packet, err := proto.ReadPacket(context.TODO(), &buffer)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tc.packet, packet)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadPacketChecksumError(t *testing.T) {
|
||||
// Create a simple packet with an invalid Checksum
|
||||
var buffer bytes.Buffer
|
||||
invalidPacket := []uint8{
|
||||
proto.SOF,
|
||||
0x01,
|
||||
0x11,
|
||||
0x22,
|
||||
0x33,
|
||||
0x00,
|
||||
proto.EOF,
|
||||
} // 0x00 as checksum is invalid here
|
||||
|
||||
// Write invalid packet to buffer
|
||||
for _, b := range invalidPacket {
|
||||
_, err := buffer.Write([]uint8{b})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to write to buffer: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to read the packet with a Checksum error
|
||||
_, err := proto.ReadPacket(context.TODO(), &buffer)
|
||||
assert.ErrorIs(t, err, proto.ErrChecksumMismatch)
|
||||
}
|
||||
|
||||
func TestReadPacketDirtyReader(t *testing.T) {
|
||||
// Create a simple packet with an invalid Checksum
|
||||
var buffer bytes.Buffer
|
||||
invalidPacket := []uint8{
|
||||
// Incomplete previous packet
|
||||
0x01,
|
||||
0x12,
|
||||
0x13,
|
||||
0x11,
|
||||
proto.EOF,
|
||||
// Actual packet
|
||||
proto.SOF,
|
||||
0x01,
|
||||
0x11,
|
||||
0x12,
|
||||
0x13,
|
||||
0x11,
|
||||
proto.EOF,
|
||||
}
|
||||
|
||||
// Write invalid packet to buffer
|
||||
for _, b := range invalidPacket {
|
||||
_, err := buffer.Write([]uint8{b})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to write to buffer: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to read the packet with a Checksum error
|
||||
pkt, err := proto.ReadPacket(context.TODO(), &buffer)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, proto.Packet{Command: proto.Command(0x01), Data: proto.Data{0x11, 0x12, 0x13}}, pkt)
|
||||
}
|
||||
22
pkg/smartfanunit/smartfanunit.go
Normal file
22
pkg/smartfanunit/smartfanunit.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package smartfanunit
|
||||
|
||||
import (
|
||||
"github.com/xvzf/computeblade-agent/pkg/smartfanunit/proto"
|
||||
)
|
||||
|
||||
const (
|
||||
Baudrate = 115200
|
||||
)
|
||||
|
||||
func MatchCmd(cmd proto.Command) func(any) bool {
|
||||
return func(pktAny any) bool {
|
||||
pkt, ok := pktAny.(proto.Packet)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if pkt.Command == cmd {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user