Files
compute-blade-agent/cmd/fanunit/controller.go
Cedric Kienzler 631ddfedd4 chore: update repository references from uptime-industries to computeblade-community (#70)
* chore: update repository references from uptime-industries to compute-blade-community

chore: update repository references from uptime-industries to compute-blade-community for consistency and clarity across all files
fix: update links in CHANGELOG.md and README.md to point to the new repository location for accurate documentation
fix: update Dockerfile and systemd service file to reflect the new repository URL for proper source tracking
refactor: change import paths in Go files to use the new repository name for correct package referencing

* chore: Add CODEOWNERS

* feat: add auto-labeling

---------

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

263 lines
7.1 KiB
Go

//go:build tinygo
package main
import (
"context"
"time"
"machine"
"github.com/compute-blade-community/compute-blade-agent/pkg/events"
"github.com/compute-blade-community/compute-blade-agent/pkg/hal/led"
"github.com/compute-blade-community/compute-blade-agent/pkg/smartfanunit"
"github.com/compute-blade-community/compute-blade-agent/pkg/smartfanunit/emc2101"
"github.com/compute-blade-community/compute-blade-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 events.EventBus
leftLed led.Color
rightLed led.Color
leftReqFanSpeed uint8
rightReqFanSpeed uint8
buttonPressed int
}
func (c *Controller) Run(parentCtx context.Context) error {
c.eb = events.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 (right)")
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 events
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 events 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, events.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, events.MatchAll)
defer subLeft.Unsubscribe()
subRight := c.eb.Subscribe(rightBladeTopicIn, 1, events.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
}
}
}