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

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

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

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

when having multiple compute-blades in your bladeconfig:

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

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

* test: improve unit-testing

* fix: pin github.com/warthog618/gpiod

---------

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

168 lines
4.3 KiB
Go

package ledengine
import (
"context"
"errors"
"time"
"github.com/compute-blade-community/compute-blade-agent/pkg/hal"
"github.com/compute-blade-community/compute-blade-agent/pkg/hal/led"
"github.com/compute-blade-community/compute-blade-agent/pkg/util"
)
// LedEngine is the interface for controlling effects on the computeblade RGB LEDs
type LedEngine interface {
// SetPattern sets the blink pattern
SetPattern(pattern BlinkPattern) error
// Run runs the LED Engine
Run(ctx context.Context) error
}
// ledEngineImpl is the implementation of the LedEngine interface
type ledEngineImpl struct {
ledIdx hal.LedIndex
restart chan struct{}
pattern BlinkPattern
hal hal.ComputeBladeHal
clock util.Clock
}
type BlinkPattern struct {
// BaseColor is the color is the color shown when the pattern starts (-> before the first blink)
BaseColor led.Color
// ActiveColor is the color shown when the pattern is active (-> during the blink)
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
}
func mapBrightnessUint8(brightness float64) uint8 {
return uint8(255.0 * brightness)
}
func LedColorPurple(brightness float64) led.Color {
return led.Color{
Red: mapBrightnessUint8(brightness),
Green: 0,
Blue: mapBrightnessUint8(brightness),
}
}
func LedColorRed(brightness float64) led.Color {
return led.Color{
Red: mapBrightnessUint8(brightness),
Green: 0,
Blue: 0,
}
}
func LedColorGreen(brightness float64) led.Color {
return led.Color{
Red: 0,
Green: mapBrightnessUint8(brightness),
Blue: 0,
}
}
// NewStaticPattern creates a new static pattern (no color changes)
func NewStaticPattern(color led.Color) BlinkPattern {
return BlinkPattern{
BaseColor: color,
ActiveColor: color,
Delays: []time.Duration{time.Hour}, // 1h delay, we don't care as there are no color changes involved
}
}
// NewBurstPattern creates a new burst pattern (~1s cycle duration with 3x 50ms bursts)
func NewBurstPattern(baseColor led.Color, burstColor led.Color) BlinkPattern {
return BlinkPattern{
BaseColor: baseColor,
ActiveColor: burstColor,
Delays: []time.Duration{
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 led.Color, activeColor led.Color) BlinkPattern {
return BlinkPattern{
BaseColor: baseColor,
ActiveColor: activeColor,
Delays: []time.Duration{
time.Second, // 1s off
time.Second, // 1s on
},
}
}
func New(hal hal.ComputeBladeHal, ledIdx hal.LedIndex) LedEngine {
return NewLedEngine(Options{
Hal: hal,
LedIdx: ledIdx,
})
}
func NewLedEngine(opts Options) LedEngine {
clock := opts.Clock
if clock == nil {
clock = util.RealClock{}
}
return &ledEngineImpl{
ledIdx: opts.LedIdx,
hal: opts.Hal,
restart: make(chan struct{}), // restart channel controls cancellation of any pattern
pattern: NewStaticPattern(led.Color{}), // Turn off LEDs by default
clock: clock,
}
}
func (b *ledEngineImpl) SetPattern(pattern BlinkPattern) error {
if len(pattern.Delays) == 0 {
return errors.New("pattern must have at least one delay")
}
b.pattern = pattern
close(b.restart)
b.restart = make(chan struct{})
return nil
}
// Run runs the blink engine
func (b *ledEngineImpl) Run(ctx context.Context) error {
// Iterate forever unless context is done
for {
// Set the base color
if err := b.hal.SetLed(b.ledIdx, b.pattern.BaseColor); err != nil {
return err
}
// Iterate through pattern delays
PatternLoop:
for idx, delay := range b.pattern.Delays {
select {
// Whenever the pattern is restarted, break the loop and start over
case <-b.restart:
break PatternLoop
// Whenever the context is done, return
case <-ctx.Done():
return ctx.Err()
// Whenever the delay is over, change the color
case <-b.clock.After(delay):
color := b.pattern.BaseColor
if idx%2 == 0 {
color = b.pattern.ActiveColor
}
if err := b.hal.SetLed(b.ledIdx, color); err != nil {
return err
}
}
}
}
}