Compare commits

...

58 Commits

Author SHA1 Message Date
renovate[bot]
1161cf3ea2 chore(deps): Update marocchino/sticky-pull-request-comment action to v3 2026-04-01 16:55:19 +00:00
renovate[bot]
42faaf332e chore(deps): Update module google.golang.org/grpc to v1.80.0 (#173)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-01 13:11:51 +00:00
renovate[bot]
33458c075d chore(deps): Update module google.golang.org/grpc to v1.79.3 [SECURITY] (#172)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-19 10:09:40 +00:00
renovate[bot]
d09abe14f2 chore(deps): Update module github.com/olekukonko/tablewriter to v1.1.4 (#169)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-12 18:08:17 +00:00
renovate[bot]
baa7f813c1 chore(deps): Update fgrosse/go-coverage-report action to v1.3.0 (#168)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-12 06:00:23 +00:00
renovate[bot]
bc86e413fc chore(deps): Update module golang.org/x/sync to v0.20.0 (#167)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-08 17:55:20 +00:00
renovate[bot]
105d39be89 chore(deps): Update module google.golang.org/grpc to v1.79.2 (#166)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-06 13:44:51 +00:00
github-actions[bot]
1f6fecfefe chore(main): release 0.11.2 (#164)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-04 15:33:04 +01:00
weslson
03541febb2 feat(hal): add RK3588 (Radxa CM5) HAL with sysfs fan control (#155)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Cedric Specht <cedric@specht-labs.de>
2026-03-04 15:30:45 +01:00
renovate[bot]
ed39f8320b chore(deps): Update docker/setup-qemu-action action to v4 (#162)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-04 15:29:21 +01:00
renovate[bot]
6c1fb4aeb6 chore(deps): Update github.com/sierrasoftworks/humane-errors-go digest to a7be4ff (#161)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-04 15:29:05 +01:00
renovate[bot]
75c7df898f chore(deps): Update GitHub Artifact Actions (#150)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-04 15:28:42 +01:00
renovate[bot]
2b2aab20d4 chore(deps): Update goreleaser/goreleaser-action action to v7 (#159)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-04 15:28:29 +01:00
renovate[bot]
e8b9d888fa chore(deps): Update docker/login-action action to v4 (#163)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-04 15:28:11 +01:00
weslson
84346089ca fix(fancontroller): support more than 2 steps in fan curve (#156)
* fix(fancontroller): support more than 2 steps in fan curve

The GetFanSpeedPercent function only used the first 2 steps from the
config, ignoring all subsequent steps. This caused the fan to cap at
step[1]'s percent value regardless of actual temperature.

For example, with a typical 5-step curve:
  - 40°C → 30%
  - 50°C → 50%
  - 60°C → 70%
  - 70°C → 90%
  - 75°C → 100%

Any temperature ≥50°C would return 50% instead of the correct value.
At 77°C the fan should be at 100%, but was stuck at 50%.

The fix properly iterates through all configured steps to find the
correct temperature bracket and interpolates within it.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Apply suggestions from code review

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Cedric Specht <cedric@specht-labs.de>
2026-03-04 15:26:29 +01:00
weslson
9477cc71c2 feat(hal): add BCM2712 (CM5/Pi 5) HAL support (#154)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Cedric Specht <cedric@specht-labs.de>
2026-03-04 15:21:39 +01:00
renovate[bot]
0c8efc3e54 chore(deps): Update module google.golang.org/grpc to v1.79.1 (#158)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-13 13:56:03 +00:00
renovate[bot]
d3018868c8 chore(deps): Update module google.golang.org/grpc to v1.79.0 (#157)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-12 21:52:40 +00:00
renovate[bot]
85a65ff4fc chore(deps): Update module github.com/olekukonko/tablewriter to v1.1.3 (#153)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-17 00:38:16 +00:00
renovate[bot]
217a796fb9 chore(deps): Update module google.golang.org/grpc to v1.78.0 (#152)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-23 14:33:59 +00:00
renovate[bot]
4772faaa46 chore(deps): Update module tinygo.org/x/drivers to v0.34.0 (#151)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-15 22:09:22 +00:00
renovate[bot]
764a530c67 chore(deps): Update module google.golang.org/protobuf to v1.36.11 (#149)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-12 15:30:59 +00:00
renovate[bot]
8459b80451 chore(deps): Update module golang.org/x/sync to v0.19.0 (#148)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-08 23:36:46 +00:00
renovate[bot]
7508715397 chore(deps): Update module github.com/spf13/cobra to v1.10.2 (#147)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-04 07:28:42 +00:00
renovate[bot]
659ef04d3f chore(deps): Update module github.com/olekukonko/tablewriter to v1.1.2 (#146)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-30 17:10:23 +00:00
renovate[bot]
3b8362a640 chore(deps): Update actions/setup-go action to v6 (#131) 2025-11-26 22:22:12 +00:00
renovate[bot]
0ae498cfd2 chore(deps): Update amannn/action-semantic-pull-request action to v6 (#117) 2025-11-26 08:49:23 +00:00
renovate[bot]
2e75867a2c chore(deps): Update actions/labeler action to v6 (#129) 2025-11-26 08:43:49 +00:00
renovate[bot]
da33d0767a chore(deps): Update GitHub Artifact Actions (#112) 2025-11-26 08:38:32 +00:00
renovate[bot]
940b8851f3 chore(deps): Update github.com/sierrasoftworks/humane-errors-go digest to 6b4ca9d (#116) 2025-11-26 08:32:54 +00:00
renovate[bot]
9d37e4d0e0 chore(deps): Update golangci/golangci-lint-action action to v9 (#140) 2025-11-26 08:27:18 +00:00
renovate[bot]
ee845c96da chore(deps): Update actions/checkout action to v6 (#145) 2025-11-26 09:22:19 +01:00
renovate[bot]
682d15abe1 chore(deps): Update module go.uber.org/zap to v1.27.1 (#144)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-20 05:54:51 +00:00
renovate[bot]
c891384ed3 chore(deps): Update module google.golang.org/grpc to v1.77.0 (#143)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-18 22:44:40 +00:00
renovate[bot]
731de17934 chore(deps): Update module github.com/olekukonko/tablewriter to v1.1.1 (#142)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-09 14:32:55 +00:00
renovate[bot]
20af75e36a chore(deps): Update module golang.org/x/sync to v0.18.0 (#141)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-08 18:40:21 +00:00
renovate[bot]
bca920c510 chore(deps): Update module github.com/grpc-ecosystem/go-grpc-middleware/v2 to v2.3.3 (#139)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-05 01:51:40 +00:00
renovate[bot]
4610f83e56 chore(deps): Update module google.golang.org/grpc to v1.76.0 (#138)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-07 05:07:26 +00:00
renovate[bot]
10839c5bf6 chore(deps): Update module google.golang.org/protobuf to v1.36.10 (#137)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-02 14:46:11 +00:00
renovate[bot]
c6c7166a87 chore(deps): Update module github.com/olekukonko/tablewriter to v1.1.0 (#136)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-21 21:15:11 +00:00
renovate[bot]
19c58fda60 chore(deps): Update module golang.org/x/sync to v0.17.0 (#135)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-21 01:57:37 +00:00
renovate[bot]
551c3b50ba chore(deps): Update module github.com/spf13/viper to v1.21.0 (#134)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-20 20:42:31 +00:00
renovate[bot]
4f56e1a568 chore(deps): Update module github.com/spf13/cobra to v1.10.1 (#133)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-20 17:28:42 +00:00
renovate[bot]
de8ca4c27b chore(deps): Update module google.golang.org/protobuf to v1.36.9 (#132)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-20 13:38:55 +00:00
renovate[bot]
339b6881f2 chore(deps): Update module google.golang.org/grpc to v1.75.1 (#130)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-20 09:07:40 +00:00
renovate[bot]
7e2cb1d9c3 chore(deps): Update module github.com/spf13/pflag to v1.0.10 (#128)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-20 04:27:33 +00:00
renovate[bot]
c30a64acd9 chore(deps): Update module github.com/spechtlabs/go-otel-utils/otelzap to v0.0.15 (#127)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-20 01:15:17 +00:00
renovate[bot]
055fcb98b2 chore(deps): Update module github.com/spechtlabs/go-otel-utils/otelprovider to v0.0.15 (#126)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-19 21:52:32 +00:00
renovate[bot]
a4e4e468d1 chore(deps): Update module github.com/prometheus/client_golang to v1.23.2 (#125)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-19 17:25:50 +00:00
renovate[bot]
a710b17abf chore(deps): Update module github.com/stretchr/testify to v1.11.1 (#124)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-27 23:30:10 +00:00
renovate[bot]
eaf26cabd2 chore(deps): Update module github.com/stretchr/testify to v1.11.0 (#123)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-25 05:34:52 +00:00
renovate[bot]
b0da67eaae chore(deps): Update module github.com/spechtlabs/go-otel-utils/otelzap to v0.0.13 (#122)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-25 00:49:46 +00:00
renovate[bot]
0cfdb935b5 chore(deps): Update module github.com/spechtlabs/go-otel-utils/otelprovider to v0.0.13 (#121)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-24 20:50:37 +00:00
renovate[bot]
2679c21423 chore(deps): Update module google.golang.org/protobuf to v1.36.8 (#120)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-21 00:54:35 +00:00
renovate[bot]
4f3bea45fe chore(deps): Update module tinygo.org/x/drivers to v0.33.0 (#119)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-20 01:54:31 +00:00
renovate[bot]
a2f06d99ec chore(deps): Update module google.golang.org/protobuf to v1.36.7 (#113)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-07 17:23:04 +00:00
renovate[bot]
24cb5f0d4a chore(deps): Update module github.com/prometheus/client_golang to v1.23.0 (#111)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-01 00:39:48 +00:00
renovate[bot]
19805dd5ae chore(deps): Update module github.com/olekukonko/tablewriter to v1.0.9 (#110)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-28 23:27:58 +00:00
16 changed files with 1407 additions and 93 deletions

View File

@@ -24,18 +24,18 @@ jobs:
steps:
# Checkout code
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
# Set up Go environment
- name: Setup Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version-file: "go.mod"
cache-dependency-path: "**/*.sum"
cache: true
- name: GolangCI Lint
uses: golangci/golangci-lint-action@v8
uses: golangci/golangci-lint-action@v9
with:
version: latest
@@ -54,11 +54,11 @@ jobs:
steps:
# Checkout code
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
# Set up Go environment
- name: Setup Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version-file: "go.mod"
cache: true
@@ -68,7 +68,7 @@ jobs:
run: go test -cover -coverprofile=coverage.txt ./...
- name: Archive code coverage results
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: code-coverage
path: "coverage.txt"
@@ -87,7 +87,7 @@ jobs:
pull-requests: write # write permission needed to comment on PR
steps:
- uses: fgrosse/go-coverage-report@v1.2.0
- uses: fgrosse/go-coverage-report@v1.3.0
with:
coverage-artifact-name: "code-coverage"
coverage-file-name: "coverage.txt"
@@ -101,12 +101,12 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version-file: "go.mod"
cache-dependency-path: "**/*.sum"
@@ -122,7 +122,7 @@ jobs:
run: make build-fanunit
- name: Archive FanUnit Firmware
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: fanunit.uf2
path: "fanunit.uf2"
@@ -134,7 +134,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
@@ -144,7 +144,7 @@ jobs:
# Install GoLang
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version-file: "go.mod"
cache-dependency-path: "**/*.sum"
@@ -152,20 +152,20 @@ jobs:
# Setup docker buildx
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# Download FanUnit Firmware
- name: Download fanunit firmware
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
pattern: fanunit.uf2
# Run goreleaser
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
uses: goreleaser/goreleaser-action@v7
with:
version: latest
args: release --snapshot --clean --skip sign
@@ -173,7 +173,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload build artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: compute-blade-agent
path: dist/*-SNAPSHOT-*

View File

@@ -26,12 +26,12 @@ jobs:
contents: read
steps:
- uses: amannn/action-semantic-pull-request@v5
- uses: amannn/action-semantic-pull-request@v6
id: lint_pr_title
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: marocchino/sticky-pull-request-comment@v2
- uses: marocchino/sticky-pull-request-comment@v3
# When the previous steps fail, the workflow would stop. By adding this
# condition you can continue the execution with the populated error message.
if: always() && (steps.lint_pr_title.outputs.error_message != null)
@@ -50,7 +50,7 @@ jobs:
# Delete a previous comment when the issue has been resolved
- if: ${{ steps.lint_pr_title.outputs.error_message == null }}
uses: marocchino/sticky-pull-request-comment@v2
uses: marocchino/sticky-pull-request-comment@v3
with:
header: pr-title-lint-error
delete: true
@@ -64,7 +64,7 @@ jobs:
pull-requests: write
steps:
- uses: actions/labeler@v5
- uses: actions/labeler@v6
id: labeler
with:
sync-labels: true

View File

@@ -14,11 +14,11 @@ jobs:
steps:
# Checkout code
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
# Set up Go environment
- name: Setup Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version-file: "go.mod"
cache: true
@@ -28,7 +28,7 @@ jobs:
run: go test -cover -coverprofile=coverage.txt ./...
- name: Archive code coverage results
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: code-coverage
path: "coverage.txt"
@@ -62,12 +62,12 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version-file: "go.mod"
cache-dependency-path: "**/*.sum"
@@ -83,7 +83,7 @@ jobs:
run: make build-fanunit
- name: Archive FanUnit Firmware
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: fanunit.uf2
path: "fanunit.uf2"
@@ -99,7 +99,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
@@ -109,7 +109,7 @@ jobs:
# Install GoLang
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version-file: "go.mod"
cache-dependency-path: "**/*.sum"
@@ -117,13 +117,13 @@ jobs:
# Setup docker buildx
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: "Login to GitHub Container Registry"
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{github.actor}}
@@ -131,13 +131,13 @@ jobs:
# Download FanUnit Firmware
- name: Download fanunit firmware
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
pattern: fanunit.uf2
# Run goreleaser
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
uses: goreleaser/goreleaser-action@v7
with:
version: "~> v2"
args: release --clean

6
.gitignore vendored
View File

@@ -186,4 +186,8 @@ Temporary Items
.history
.ionide
# End of https://www.toptal.com/developers/gitignore/api/go,visualstudiocode,goland+all,macos,linux
# End of https://www.toptal.com/developers/gitignore/api/go,visualstudiocode,goland+all,macos,linux
# Build artifacts
agent
bladectl
compute-blade-agent

View File

@@ -1,5 +1,18 @@
# Changelog
## [0.11.2](https://github.com/compute-blade-community/compute-blade-agent/compare/v0.11.1...v0.11.2) (2026-03-04)
### Features
* **hal:** add BCM2712 (CM5/Pi 5) HAL support ([#154](https://github.com/compute-blade-community/compute-blade-agent/issues/154)) ([9477cc7](https://github.com/compute-blade-community/compute-blade-agent/commit/9477cc71c200cf193e467a8d543d8970e733a74f))
* **hal:** add RK3588 (Radxa CM5) HAL with sysfs fan control ([#155](https://github.com/compute-blade-community/compute-blade-agent/issues/155)) ([03541fe](https://github.com/compute-blade-community/compute-blade-agent/commit/03541febb2c02ee5cf3d432128e756de4e68dfd4))
### Bug Fixes
* **fancontroller:** support more than 2 steps in fan curve ([#156](https://github.com/compute-blade-community/compute-blade-agent/issues/156)) ([8434608](https://github.com/compute-blade-community/compute-blade-agent/commit/84346089caf7cb1b163e9279bdf9fe4cf0519651))
## [0.11.1](https://github.com/compute-blade-community/compute-blade-agent/compare/v0.11.0...v0.11.1) (2025-07-23)

101
go.mod
View File

@@ -1,91 +1,98 @@
module github.com/compute-blade-community/compute-blade-agent
go 1.24.0
go 1.25.0
require (
github.com/charmbracelet/lipgloss v1.1.0
github.com/gizak/termui/v3 v3.1.0
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2
github.com/olekukonko/tablewriter v1.0.8
github.com/prometheus/client_golang v1.22.0
github.com/sierrasoftworks/humane-errors-go v0.0.0-20250507223502-4bb667dc1e16
github.com/spechtlabs/go-otel-utils/otelprovider v0.0.10
github.com/spechtlabs/go-otel-utils/otelzap v0.0.10
github.com/spf13/cobra v1.9.1
github.com/spf13/pflag v1.0.7
github.com/spf13/viper v1.20.1
github.com/stretchr/testify v1.10.0
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3
github.com/olekukonko/tablewriter v1.1.4
github.com/prometheus/client_golang v1.23.2
github.com/sierrasoftworks/humane-errors-go v0.0.0-20260226124905-a7be4ffe4f32
github.com/spechtlabs/go-otel-utils/otelprovider v0.0.15
github.com/spechtlabs/go-otel-utils/otelzap v0.0.15
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1
github.com/warthog618/gpiod v0.8.1
go.bug.st/serial v1.6.4
go.uber.org/zap v1.27.0
golang.org/x/sync v0.16.0
google.golang.org/grpc v1.74.2
google.golang.org/protobuf v1.36.6
go.uber.org/zap v1.27.1
golang.org/x/sync v0.20.0
google.golang.org/grpc v1.80.0
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
tinygo.org/x/drivers v0.32.0
tinygo.org/x/drivers v0.34.0
)
require (
github.com/aws/smithy-go v1.22.3 // indirect
github.com/aws/smithy-go v1.22.5 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/x/ansi v0.8.0 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/clipperhouse/displaywidth v0.10.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.6.0 // indirect
github.com/creack/goselect v0.1.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fatih/color v1.15.0 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d // indirect
github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6 // indirect
github.com/olekukonko/ll v0.0.8 // indirect
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
github.com/olekukonko/errors v1.2.0 // indirect
github.com/olekukonko/ll v0.1.6 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.63.0 // indirect
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sagikazarmark/locafero v0.9.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.14.0 // indirect
github.com/spf13/cast v1.8.0 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel v1.36.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.11.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.11.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 // indirect
go.opentelemetry.io/otel/log v0.11.0 // indirect
go.opentelemetry.io/otel/metric v1.36.0 // indirect
go.opentelemetry.io/otel/sdk v1.36.0 // indirect
go.opentelemetry.io/otel/sdk/log v0.11.0 // indirect
go.opentelemetry.io/otel/trace v1.36.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.13.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.13.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 // indirect
go.opentelemetry.io/otel/log v0.13.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/sdk v1.39.0 // indirect
go.opentelemetry.io/otel/sdk/log v0.13.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.25.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.33.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect
)

245
go.sum
View File

@@ -1,11 +1,15 @@
github.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k=
github.com/aws/smithy-go v1.22.3/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
github.com/aws/smithy-go v1.22.5 h1:P9ATCXPMb2mPjYBgueqJNCA5S9UfktsW0tTxi+a7eqw=
github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
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/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
@@ -18,6 +22,22 @@ github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0G
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/clipperhouse/displaywidth v0.3.1 h1:k07iN9gD32177o1y4O1jQMzbLdCrsGJh+blirVYybsk=
github.com/clipperhouse/displaywidth v0.3.1/go.mod h1:tgLJKKyaDOCadywag3agw4snxS5kYEuYR6Y9+qWDDYM=
github.com/clipperhouse/displaywidth v0.6.0 h1:k32vueaksef9WIKCNcoqRNyKbyvkvkysNYnAWz2fN4s=
github.com/clipperhouse/displaywidth v0.6.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
github.com/clipperhouse/displaywidth v0.6.2 h1:ZDpTkFfpHOKte4RG5O/BOyf3ysnvFswpyYrV7z2uAKo=
github.com/clipperhouse/displaywidth v0.6.2/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
github.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g=
github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos=
github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/goselect v0.1.3 h1:MaGNMclRo7P2Jl21hBpR1Cn33ITSbKP6E49RtfblLKc=
github.com/creack/goselect v0.1.3/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY=
@@ -25,6 +45,8 @@ 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=
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
@@ -40,6 +62,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
@@ -50,8 +74,12 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 h1:sGm2vDRFUrQJO/Veii4h4zG2vvqG6uWNkBHSTqXOZk0=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQgbf7ZkG1hhSOXDhhn4MLTknx2aAc=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
@@ -66,12 +94,16 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
@@ -80,14 +112,42 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d h1:x3S6kxmy49zXVVyhcnrFqxvNVCBPb2KZ9hV2RBdS840=
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6 h1:r3FaAI0NZK3hSmtTDrBVREhKULp8oUeqLT5Eyl2mSPo=
github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo=
github.com/olekukonko/errors v1.2.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/ll v0.0.8 h1:sbGZ1Fx4QxJXEqL/6IG8GEFnYojUSQ45dJVwN2FH2fc=
github.com/olekukonko/ll v0.0.8/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
github.com/olekukonko/ll v0.1.2 h1:lkg/k/9mlsy0SxO5aC+WEpbdT5K83ddnNhAepz7TQc0=
github.com/olekukonko/ll v0.1.2/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew=
github.com/olekukonko/ll v0.1.3 h1:sV2jrhQGq5B3W0nENUISCR6azIPf7UBUpVq0x/y70Fg=
github.com/olekukonko/ll v0.1.3/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew=
github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0 h1:jrYnow5+hy3WRDCBypUFvVKNSPPCdqgSXIE9eJDD8LM=
github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew=
github.com/olekukonko/ll v0.1.6 h1:lGVTHO+Qc4Qm+fce/2h2m5y9LvqaW+DCN7xW9hsU3uA=
github.com/olekukonko/ll v0.1.6/go.mod h1:NVUmjBb/aCtUpjKk75BhWrOlARz3dqsM+OtszpY4o88=
github.com/olekukonko/tablewriter v1.0.7 h1:HCC2e3MM+2g72M81ZcJU11uciw6z/p82aEnm4/ySDGw=
github.com/olekukonko/tablewriter v1.0.7/go.mod h1:H428M+HzoUXC6JU2Abj9IT9ooRmdq9CxuDmKMtrOCMs=
github.com/olekukonko/tablewriter v1.0.8 h1:f6wJzHg4QUtJdvrVPKco4QTrAylgaU0+b9br/lJxEiQ=
github.com/olekukonko/tablewriter v1.0.8/go.mod h1:H428M+HzoUXC6JU2Abj9IT9ooRmdq9CxuDmKMtrOCMs=
github.com/olekukonko/tablewriter v1.0.9 h1:XGwRsYLC2bY7bNd93Dk51bcPZksWZmLYuaTHR0FqfL8=
github.com/olekukonko/tablewriter v1.0.9/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
github.com/olekukonko/tablewriter v1.1.0 h1:N0LHrshF4T39KvI96fn6GT8HEjXRXYNDrDjKFDB7RIY=
github.com/olekukonko/tablewriter v1.1.0/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
github.com/olekukonko/tablewriter v1.1.1 h1:b3reP6GCfrHwmKkYwNRFh2rxidGHcT6cgxj/sHiDDx0=
github.com/olekukonko/tablewriter v1.1.1/go.mod h1:De/bIcTF+gpBDB3Alv3fEsZA+9unTsSzAg/ZGADCtn4=
github.com/olekukonko/tablewriter v1.1.2 h1:L2kI1Y5tZBct/O/TyZK1zIE9GlBj/TVs+AY5tZDCDSc=
github.com/olekukonko/tablewriter v1.1.2/go.mod h1:z7SYPugVqGVavWoA2sGsFIoOVNmEHxUAAMrhXONtfkg=
github.com/olekukonko/tablewriter v1.1.3 h1:VSHhghXxrP0JHl+0NnKid7WoEmd9/urKRJLysb70nnA=
github.com/olekukonko/tablewriter v1.1.3/go.mod h1:9VU0knjhmMkXjnMKrZ3+L2JhhtsQ/L38BbL3CRNE8tM=
github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I=
github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pilebones/go-udev v0.9.0 h1:N1uEO/SxUwtIctc0WLU0t69JeBxIYEYnj8lT/Nabl9Q=
@@ -96,10 +156,18 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@@ -110,30 +178,67 @@ github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWN
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k=
github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/sierrasoftworks/humane-errors-go v0.0.0-20250507223502-4bb667dc1e16 h1:9vtY3febGroV+aPR5OlI3fekkesi+lMVsVWyxBp/rfk=
github.com/sierrasoftworks/humane-errors-go v0.0.0-20250507223502-4bb667dc1e16/go.mod h1:CbJLj9L1qHdzLg4YRh2Lzr0noe9pR6QrVEqfLbITRKw=
github.com/sierrasoftworks/humane-errors-go v0.0.0-20250811205537-5f14a04ebff5 h1:nlfxPheTxwOE5hEq9iVurbo83/Wie52V5lKIhi73mRw=
github.com/sierrasoftworks/humane-errors-go v0.0.0-20250811205537-5f14a04ebff5/go.mod h1:CbJLj9L1qHdzLg4YRh2Lzr0noe9pR6QrVEqfLbITRKw=
github.com/sierrasoftworks/humane-errors-go v0.0.0-20251121131909-6b4ca9dd07a7 h1:mD7GjWbIAdxIvDwTXs2uidulnIAfi06h7SZBQvDZga8=
github.com/sierrasoftworks/humane-errors-go v0.0.0-20251121131909-6b4ca9dd07a7/go.mod h1:AyTGN4SGjbmEHvVAnBy8SpCnG3hskAfZ7TJW1AzMXgk=
github.com/sierrasoftworks/humane-errors-go v0.0.0-20260226124905-a7be4ffe4f32 h1:6qDjIFnVHw6zdtW0t3RuN1PWcVSL6kXZiiDYR/P7id8=
github.com/sierrasoftworks/humane-errors-go v0.0.0-20260226124905-a7be4ffe4f32/go.mod h1:AyTGN4SGjbmEHvVAnBy8SpCnG3hskAfZ7TJW1AzMXgk=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/spechtlabs/go-otel-utils/otelprovider v0.0.10 h1:Q5p+5KGA587GfzR6FdXGje4XBfxhi1u4NSu6lSnWCGA=
github.com/spechtlabs/go-otel-utils/otelprovider v0.0.10/go.mod h1:sFuJXEBbNq/pQx9pP5OnVtx9yGJnH4fXi7x3qihW0ak=
github.com/spechtlabs/go-otel-utils/otelprovider v0.0.13 h1:DyTnvzLrhGowglrmDO8GFzbC/ZnzHRkMlFCk7p/pEmc=
github.com/spechtlabs/go-otel-utils/otelprovider v0.0.13/go.mod h1:63JphMzIY2vGTZ9OMjntS/XUM2sVwLCydSsjVE+M5QU=
github.com/spechtlabs/go-otel-utils/otelprovider v0.0.15 h1:LRx9EFzD4bI+kH3NNn6GtqwRrGbJgmyau4IOJcfBTYE=
github.com/spechtlabs/go-otel-utils/otelprovider v0.0.15/go.mod h1:ACquqruOxYFRL5H7TCxXKXIJTVj+6eqlEJR6WQAEBeE=
github.com/spechtlabs/go-otel-utils/otelzap v0.0.10 h1:RR/WS4b+ABxNL7xzlK4FTvnuXRbGk3yyggvvLnQ+FeM=
github.com/spechtlabs/go-otel-utils/otelzap v0.0.10/go.mod h1:IhsBuW+sZwLxX1Ww5LmTlIonBP8GiyhsiZkIRq+ySE0=
github.com/spechtlabs/go-otel-utils/otelzap v0.0.11 h1:D3jzku3MMLMt/CgI++Zk3TE/mnQWz9dI9y/sCBb60OA=
github.com/spechtlabs/go-otel-utils/otelzap v0.0.11/go.mod h1:Bk/uRNkU1/MsMc2J1NUxF+0lRNDbUUIulioJevLYM74=
github.com/spechtlabs/go-otel-utils/otelzap v0.0.13 h1:eKrf3Jd4WurABA+iw4TAZJRF2/Ke1cIzsX1wSUOZZT0=
github.com/spechtlabs/go-otel-utils/otelzap v0.0.13/go.mod h1:Bk/uRNkU1/MsMc2J1NUxF+0lRNDbUUIulioJevLYM74=
github.com/spechtlabs/go-otel-utils/otelzap v0.0.15 h1:LPo3vmPRVTQDG/ic2r4q1t9irptnffRRTEpaLYjbguo=
github.com/spechtlabs/go-otel-utils/otelzap v0.0.15/go.mod h1:0LC7Tzo53EdZ0FY6dUMDDkDmimphYDbjqIg+BY5MJjk=
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.8.0 h1:gEN9K4b8Xws4EX0+a0reLmhq8moKn7ntRlQYgjPeCDk=
github.com/spf13/cast v1.8.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/warthog618/gpiod v0.8.1 h1:+8iHpHd3fljAd6l4AT8jPbMDQNKdvBIpW/hmLgAcHiM=
@@ -144,68 +249,204 @@ go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A=
go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.11.0 h1:HMUytBT3uGhPKYY/u/G5MR9itrlSO2SMOsSD3Tk3k7A=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.11.0/go.mod h1:hdDXsiNLmdW/9BF2jQpnHHlhFajpWCEYfM6e5m2OAZg=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.13.0 h1:z6lNIajgEBVtQZHjfw2hAccPEBDs+nx58VemmXWa2ec=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.13.0/go.mod h1:+kyc3bRx/Qkq05P6OCu3mTEIOxYRYzoIg+JsUp5X+PM=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.11.0 h1:C/Wi2F8wEmbxJ9Kuzw/nhP+Z9XaHYMkyDmXy6yR2cjw=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.11.0/go.mod h1:0Lr9vmGKzadCTgsiBydxr6GEZ8SsZ7Ks53LzjWG5Ar4=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.13.0 h1:zUfYw8cscHHLwaY8Xz3fiJu+R59xBnkgq2Zr1lwmK/0=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.13.0/go.mod h1:514JLMCcFLQFS8cnTepOk6I09cKWJ5nGHBxHrMJ8Yfg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 h1:EtFWSnwW9hGObjkIdmlnWSydO+Qs8OwzfzXLUPg4xOc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 h1:bDMKF3RUSxshZ5OjOTi8rsHGaPKsAt76FaqgvIUySLc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0/go.mod h1:dDT67G/IkA46Mr2l9Uj7HsQVwsjASyV9SjGofsiUZDA=
go.opentelemetry.io/otel/log v0.11.0 h1:c24Hrlk5WJ8JWcwbQxdBqxZdOK7PcP/LFtOtwpDTe3Y=
go.opentelemetry.io/otel/log v0.11.0/go.mod h1:U/sxQ83FPmT29trrifhQg+Zj2lo1/IPN1PF6RTFqdwc=
go.opentelemetry.io/otel/log v0.13.0 h1:yoxRoIZcohB6Xf0lNv9QIyCzQvrtGZklVbdCoyb7dls=
go.opentelemetry.io/otel/log v0.13.0/go.mod h1:INKfG4k1O9CL25BaM1qLe0zIedOpvlS5Z7XgSbmN83E=
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/log v0.11.0 h1:7bAOpjpGglWhdEzP8z0VXc4jObOiDEwr3IYbhBnjk2c=
go.opentelemetry.io/otel/sdk/log v0.11.0/go.mod h1:dndLTxZbwBstZoqsJB3kGsRPkpAgaJrWfQg3lhlHFFY=
go.opentelemetry.io/otel/sdk/log v0.13.0 h1:I3CGUszjM926OphK8ZdzF+kLqFvfRY/IIoFq/TjwfaQ=
go.opentelemetry.io/otel/sdk/log v0.13.0/go.mod h1:lOrQyCCXmpZdN7NchXb6DOZZa1N5G1R2tm5GMMTpDBw=
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
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.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d h1:0olWaB5pg3+oychR51GUVCEsGkeCU/2JxjBgIo4f3M0=
golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/net v0.46.1-0.20251013234738-63d1a5100f82 h1:6/3JGEh1C88g7m+qzzTbl3A0FtsLguXieqofVLU/JAo=
golang.org/x/net v0.46.1-0.20251013234738-63d1a5100f82/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 h1:hE3bRWtU6uceqlh4fhrSnUyjKHMKB9KrTLLG+bc0ddM=
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8=
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1:SGktgSolFCo75dnHJF2yMvnns6jCmHFJ0vE4Vn2JKvQ=
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw=
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c h1:AtEkQdl5b6zsybXcbz00j1LwNodDuH6hVifIaNqk7NQ=
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c/go.mod h1:ea2MjsO70ssTfCjiwHgI0ZFqcw45Ksuk2ckf9G468GA=
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4=
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda h1:+2XxjfsAu6vqFxwGBRcHiMaDCuZiqXGDUDVWVtrFAnE=
google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516 h1:vmC/ws+pLzWjj/gzApyoZuSVrDtF1aod4u/+bbj8hgM=
google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:p3MLuOwURrGBRoEyFHBT3GjUwaCQVKeNqqWxlcISGdw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c h1:qXWI/sQtv5UKboZ/zUk7h+mrf/lXORyI+n9DKDAusdg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda h1:i/Q+bfisr7gq6feoJnS/DlpdwEL4ihp41fvRiM3Ork0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
google.golang.org/grpc v1.74.0 h1:sxRSkyLxlceWQiqDofxDot3d4u7DyoHPc7SBXMj8gGY=
google.golang.org/grpc v1.74.0/go.mod h1:NZUaK8dAMUfzhK6uxZ+9511LtOrk73UGWOFoNvz7z+s=
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/grpc v1.79.0 h1:6/+EFlxsMyoSbHbBoEDx94n/Ycx/bi0IhJ5Qh7b7LaA=
google.golang.org/grpc v1.79.0/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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=
@@ -215,3 +456,7 @@ tinygo.org/x/drivers v0.31.0 h1:Q2RpvTRMtdmjHD2Xyn4e8WXsJZKpIny3Lg4hzG1dLu4=
tinygo.org/x/drivers v0.31.0/go.mod h1:ZdErNrApSABdVXjA1RejD67R8SNRI6RKVfYgQDZtKtk=
tinygo.org/x/drivers v0.32.0 h1:qz7MRR1ZBIUhWC6kc4XuVNPr2+mUT8m7QJwA+Jji4IU=
tinygo.org/x/drivers v0.32.0/go.mod h1:ZdErNrApSABdVXjA1RejD67R8SNRI6RKVfYgQDZtKtk=
tinygo.org/x/drivers v0.33.0 h1:5r8Ab0IxjWQi7LzYLNWpya6U4nedo9ZtxeMaAzrJTG8=
tinygo.org/x/drivers v0.33.0/go.mod h1:ZdErNrApSABdVXjA1RejD67R8SNRI6RKVfYgQDZtKtk=
tinygo.org/x/drivers v0.34.0 h1:lw8ePJeUSn9oICKBvQXHC9TIE+J00OfXfkGTrpXM9Iw=
tinygo.org/x/drivers v0.34.0/go.mod h1:ZdErNrApSABdVXjA1RejD67R8SNRI6RKVfYgQDZtKtk=

View File

@@ -54,7 +54,7 @@ type computeBladeAgent struct {
// NewComputeBladeAgent creates and initializes a new ComputeBladeAgent, including gRPC server setup and hardware interfaces.
func NewComputeBladeAgent(ctx context.Context, config agent.ComputeBladeAgentConfig, agentInfo agent.ComputeBladeAgentInfo) (agent.ComputeBladeAgent, error) {
blade, err := hal.NewCm4Hal(ctx, config.ComputeBladeHalOpts)
blade, err := hal.NewHal(ctx, config.ComputeBladeHalOpts)
if err != nil {
return nil, err
}

View File

@@ -86,20 +86,31 @@ func (f *fanControllerLinear) GetFanSpeedPercent(temperature float64) uint8 {
return f.overrideOpts.Percent
}
if temperature <= f.config.Steps[0].Temperature {
return f.config.Steps[0].Percent
}
if temperature >= f.config.Steps[1].Temperature {
return f.config.Steps[1].Percent
steps := f.config.Steps
// Below minimum temperature: use minimum fan speed
if temperature <= steps[0].Temperature {
return steps[0].Percent
}
// Calculate slope
slope := float64(f.config.Steps[1].Percent-f.config.Steps[0].Percent) / (f.config.Steps[1].Temperature - f.config.Steps[0].Temperature)
// Above maximum temperature: use maximum fan speed
lastIdx := len(steps) - 1
if temperature >= steps[lastIdx].Temperature {
return steps[lastIdx].Percent
}
// Calculate speed
speed := float64(f.config.Steps[0].Percent) + slope*(temperature-f.config.Steps[0].Temperature)
// Find the bracket where steps[i].Temperature <= temperature < steps[i+1].Temperature
for i := 0; i < lastIdx; i++ {
if temperature >= steps[i].Temperature && temperature < steps[i+1].Temperature {
// Linear interpolation between steps[i] and steps[i+1]
slope := float64(steps[i+1].Percent-steps[i].Percent) / (steps[i+1].Temperature - steps[i].Temperature)
speed := float64(steps[i].Percent) + slope*(temperature-steps[i].Temperature)
return uint8(speed)
}
}
return uint8(speed)
// Fallback (should not reach here due to above checks)
return steps[lastIdx].Percent
}
func (f *fanControllerLinear) IsAutomaticSpeed() bool {

View File

@@ -45,6 +45,55 @@ func TestFanControllerLinear_GetFanSpeed(t *testing.T) {
}
}
func TestFanControllerLinear_GetFanSpeedMultipleSteps(t *testing.T) {
t.Parallel()
// Typical 5-step fan curve configuration
config := fancontroller.Config{
Steps: []fancontroller.Step{
{Temperature: 40, Percent: 30},
{Temperature: 50, Percent: 50},
{Temperature: 60, Percent: 70},
{Temperature: 70, Percent: 90},
{Temperature: 75, Percent: 100},
},
}
controller, err := fancontroller.NewLinearFanController(config)
if err != nil {
t.Fatalf("Failed to create fan controller: %v", err)
}
testCases := []struct {
name string
temperature float64
expected uint8
}{
{"below minimum", 30, 30}, // Below 40°C: use minimum 30%
{"at step 0", 40, 30}, // At 40°C: 30%
{"between step 0-1", 45, 40}, // Midpoint 40-50°C: 40%
{"at step 1", 50, 50}, // At 50°C: 50%
{"between step 1-2", 55, 60}, // Midpoint 50-60°C: 60%
{"at step 2", 60, 70}, // At 60°C: 70%
{"between step 2-3", 65, 80}, // Midpoint 60-70°C: 80%
{"at step 3", 70, 90}, // At 70°C: 90%
{"between step 3-4", 72, 94}, // 70 + (100-90)*(72-70)/(75-70) = 90 + 4 = 94%
{"at step 4", 75, 100}, // At 75°C: 100%
{"above maximum", 80, 100}, // Above 75°C: use maximum 100%
{"well above maximum", 90, 100}, // Well above: still 100%
}
for _, tc := range testCases {
expected := tc.expected
temperature := tc.temperature
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
speed := controller.GetFanSpeedPercent(temperature)
assert.Equal(t, expected, speed, "Temperature %.1f°C should yield %d%% fan speed", temperature, expected)
})
}
}
func TestFanControllerLinear_GetFanSpeedWithOverride(t *testing.T) {
t.Parallel()

View File

@@ -97,7 +97,7 @@ type bcm2711 struct {
fanUnit FanUnit
}
func NewCm4Hal(ctx context.Context, opts ComputeBladeHalOpts) (ComputeBladeHal, error) {
func newBcm2711Hal(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 {

View File

@@ -20,7 +20,7 @@ type SimulatedHal struct {
isStealthMode bool
}
func NewCm4Hal(_ context.Context, _ ComputeBladeHalOpts) (ComputeBladeHal, error) {
func NewHal(_ context.Context, _ ComputeBladeHalOpts) (ComputeBladeHal, error) {
logger := otelzap.L().Named("hal").Named("simulated-cm4")
logger.Warn("Using simulated hal")

653
pkg/hal/hal_bcm2712.go Normal file
View File

@@ -0,0 +1,653 @@
//go:build linux && !tinygo
package hal
import (
"context"
"errors"
"fmt"
"io"
"os"
"runtime"
"strconv"
"strings"
"sync"
"syscall"
"time"
"github.com/compute-blade-community/compute-blade-agent/pkg/hal/led"
"github.com/compute-blade-community/compute-blade-agent/pkg/log"
"github.com/warthog618/gpiod"
"github.com/warthog618/gpiod/device/rpi"
"go.uber.org/zap"
"golang.org/x/sync/errgroup"
)
const (
// RP1 southbridge is connected via PCIe on BCM2712.
// The BAR base address is fixed by firmware at 0x1f00000000.
rp1BarBase int64 = 0x1f00000000
rp1GpioBase int64 = rp1BarBase + 0xd0000
rp1Pwm0Base int64 = rp1BarBase + 0x98000
rp1PageSize = 4096
// RP1 PWM input clock is 50 MHz (from device tree assigned-clock-rates)
rp1PwmClockHz = 50_000_000
// RP1 GPIO register layout: each GPIO has 8 bytes (STATUS + CTRL)
rp1GpioCtrlOffset = 0x04 // CTRL register offset within each GPIO's 8-byte block
rp1GpioRegSize = 0x08
rp1GpioFuncselMask = 0x1f // CTRL bits [4:0]
// GPIO function select values (confirmed via `pinctrl set/get` on CM5)
// GPIO 12: funcsel 0 (a0) = PWM0_CHAN0 (fan PWM)
// GPIO 18: funcsel 3 (a3) = PWM0_CHAN2 (WS281x LEDs)
rp1Gpio12FuncselPwm = 0
rp1Gpio18FuncselPwm = 3
rp1GpioFuncselSio = 5 // Software IO (standard GPIO)
rp1GpioFuncselNull = 0x1f // Null function (disabled)
// RP1 PWM register offsets (byte offsets, divide by 4 for []uint32 index)
rp1PwmGlobalCtrl = 0x00
rp1PwmFifoCtrl = 0x04
rp1PwmFifoPush = 0x08
rp1PwmFifoLevel = 0x0c
// Per-channel registers: base = 0x14 + channel * 0x10
// From kernel pwm-rp1.c: CTRL(x)=0x14+x*16, RANGE(x)=0x18+x*16, DUTY(x)=0x20+x*16
rp1PwmChanCtrlOff = 0x00 // relative to channel base
rp1PwmChanRangeOff = 0x04
rp1PwmChanPhaseOff = 0x08 // undocumented in kernel driver, possibly COUNT
rp1PwmChanDutyOff = 0x0c
rp1PwmChanSize = 0x10
rp1PwmChanBase = 0x14 // first channel base offset (NOT 0x10)
// PWM global ctrl bits (from kernel pwm-rp1.c)
rp1PwmGlobalChanEnBit = 0 // bits [3:0] = per-channel enable, BIT(x)
rp1PwmGlobalSetUpdate = 31 // BIT(31) = global set_update trigger
// PWM channel ctrl bits
rp1PwmChanCtrlModeBit = 0 // bits [1:0]
rp1PwmChanCtrlInvertBit = 2
rp1PwmChanCtrlUseFifoBit = 4
rp1PwmChanCtrlFifoPopMaskBit = 7 // bits [8:7]
// PWM modes
rp1PwmModeTrailingEdge = 0
rp1PwmModeMarkSpace = 1
rp1PwmModeSerializer = 3
// FIFO ctrl bits
rp1PwmFifoFlushBit = 5
// Fan PWM: 25 kHz for Noctua fans
// RANGE = 50 MHz / 25 kHz = 2000
rp1FanPwmRange = rp1PwmClockHz / 25000
bcm2712SmartFanUnitDev = "/dev/ttyAMA4" // UART4 on BCM2712 (same GPIO 12/13 pins as UART5 on BCM2711)
bcm2712ThermalZonePath = "/sys/class/thermal/thermal_zone0/temp"
bcm2712DebounceInterval = 100 * time.Millisecond
// WS281x LED encoding for RP1 at 50MHz with RANGE=32 serializer mode.
// Each WS281x data bit is encoded as 2 × 32-bit FIFO words (1280ns per bit ≈ 781kHz).
// Channel 2 on GPIO 18 is used independently from fan PWM on channel 0.
rp1Ws281xChan = 2 // PWM0 channel 2 on GPIO 18
rp1Ws281xRange = 32 // full 32-bit word in serializer mode
// "0" bit: T0H=400ns (20 high bits), T0L=880ns (44 low bits)
rp1Ws281xBit0Word0 uint32 = 0xFFFFF000
rp1Ws281xBit0Word1 uint32 = 0x00000000
// "1" bit: T1H=800ns (40 high bits), T1L=480ns (24 low bits)
rp1Ws281xBit1Word0 uint32 = 0xFFFFFFFF
rp1Ws281xBit1Word1 uint32 = 0xFF000000
// Reset: >50μs of low. At 640ns/word, 80 words = 51.2μs.
rp1Ws281xResetWords = 80
// Conservative FIFO depth assumption (BCM2835 has 16, RP1 may differ)
rp1Ws281xFifoMax uint32 = 8
// Safety timeout for FIFO operations
rp1Ws281xTimeout = 5 * time.Millisecond
)
// pwmChanRegIdx returns the []uint32 index for a per-channel register.
func pwmChanRegIdx(channel, regOffset int) int {
return (rp1PwmChanBase + channel*rp1PwmChanSize + regOffset) / 4
}
type bcm2712 struct {
opts ComputeBladeHalOpts
wrMutex sync.Mutex
currFanSpeed uint8
devmem *os.File
gpioMem8 []uint8
gpioMem []uint32
pwmMem8 []uint8
pwmMem []uint32
gpioChip0 *gpiod.Chip
// WS281x LED colors (top + edge)
leds [2]led.Color
// Stealth mode output
stealthModeLine *gpiod.Line
// Edge button input
edgeButtonLine *gpiod.Line
edgeButtonDebounceChan chan struct{}
edgeButtonWatchChan chan struct{}
// PoE detection input
poeLine *gpiod.Line
// Fan unit
fanUnit FanUnit
}
func newBcm2712Hal(ctx context.Context, opts ComputeBladeHalOpts) (ComputeBladeHal, error) {
devmem, err := os.OpenFile("/dev/mem", os.O_RDWR|os.O_SYNC, os.ModePerm)
if err != nil {
return nil, fmt.Errorf("failed to open /dev/mem: %w", err)
}
gpioChip0, err := gpiod.NewChip("gpiochip0")
if err != nil {
_ = devmem.Close()
return nil, fmt.Errorf("failed to open gpiochip0: %w", err)
}
// Memory-map RP1 GPIO bank 0 (GPIOs 0-27)
gpioMem, gpioMem8, err := mmap(devmem, rp1GpioBase, rp1PageSize)
if err != nil {
_ = gpioChip0.Close()
_ = devmem.Close()
return nil, fmt.Errorf("failed to mmap RP1 GPIO at 0x%x: %w", rp1GpioBase, err)
}
// Memory-map RP1 PWM0
pwmMem, pwmMem8, err := mmap(devmem, rp1Pwm0Base, rp1PageSize)
if err != nil {
_ = syscall.Munmap(gpioMem8)
_ = gpioChip0.Close()
_ = devmem.Close()
return nil, fmt.Errorf("failed to mmap RP1 PWM0 at 0x%x: %w", rp1Pwm0Base, err)
}
bcm := &bcm2712{
devmem: devmem,
gpioMem: gpioMem,
gpioMem8: gpioMem8,
pwmMem: pwmMem,
pwmMem8: pwmMem8,
gpioChip0: gpioChip0,
opts: opts,
edgeButtonDebounceChan: make(chan struct{}, 1),
edgeButtonWatchChan: make(chan struct{}),
}
computeModule.WithLabelValues("cm5").Set(1)
log.FromContext(ctx).Info("starting hal setup", zap.String("hal", "bcm2712"))
if err := bcm.setup(ctx); err != nil {
_ = bcm.Close()
return nil, err
}
return bcm, nil
}
func (bcm *bcm2712) Close() error {
errs := errors.Join(
bcm.closeFanUnit(),
bcm.unmapMem(),
bcm.closeDevmem(),
bcm.closeGpio(),
)
return errs
}
func (bcm *bcm2712) closeFanUnit() error {
if bcm.fanUnit != nil {
return bcm.fanUnit.Close()
}
return nil
}
func (bcm *bcm2712) unmapMem() error {
return errors.Join(
munmapIfNonNil(bcm.gpioMem8),
munmapIfNonNil(bcm.pwmMem8),
)
}
func munmapIfNonNil(mem []uint8) error {
if mem != nil {
return syscall.Munmap(mem)
}
return nil
}
func (bcm *bcm2712) closeDevmem() error {
if bcm.devmem != nil {
return bcm.devmem.Close()
}
return nil
}
func (bcm *bcm2712) closeGpio() error {
var errs []error
if bcm.gpioChip0 != nil {
errs = append(errs, bcm.gpioChip0.Close())
}
if bcm.poeLine != nil {
errs = append(errs, bcm.poeLine.Close())
}
if bcm.stealthModeLine != nil {
errs = append(errs, bcm.stealthModeLine.Close())
}
return errors.Join(errs...)
}
// setGpioFuncsel sets the function select for a GPIO pin via direct register write.
func (bcm *bcm2712) setGpioFuncsel(gpio int, funcsel uint32) {
ctrlIdx := (gpio*rp1GpioRegSize + rp1GpioCtrlOffset) / 4
ctrl := bcm.gpioMem[ctrlIdx]
ctrl = (ctrl &^ uint32(rp1GpioFuncselMask)) | (funcsel & uint32(rp1GpioFuncselMask))
bcm.gpioMem[ctrlIdx] = ctrl
}
func (bcm *bcm2712) setup(ctx context.Context) error {
var err error
// Register edge event handler for edge button (GPIO 20)
bcm.edgeButtonLine, err = bcm.gpioChip0.RequestLine(
rpi.GPIO20, gpiod.WithEventHandler(bcm.handleEdgeButtonEdge),
gpiod.WithFallingEdge, gpiod.WithPullUp, gpiod.WithDebounce(50*time.Millisecond))
if err != nil {
return fmt.Errorf("failed to request GPIO20 (edge button): %w", err)
}
// Register input for PoE detection (GPIO 23)
bcm.poeLine, err = bcm.gpioChip0.RequestLine(rpi.GPIO23, gpiod.AsInput, gpiod.WithPullUp)
if err != nil {
return fmt.Errorf("failed to request GPIO23 (PoE detect): %w", err)
}
// Register output for stealth mode (GPIO 21)
bcm.stealthModeLine, err = bcm.gpioChip0.RequestLine(rpi.GPIO21, gpiod.AsOutput(1))
if err != nil {
return fmt.Errorf("failed to request GPIO21 (stealth mode): %w", err)
}
// Detect fan unit type
log.FromContext(ctx).Info("detecting fan unit")
detectCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
if smartFanUnitPresent, err := SmartFanUnitPresent(detectCtx, bcm2712SmartFanUnitDev); err == nil && smartFanUnitPresent {
log.FromContext(ctx).Info("detected smart fan unit")
bcm.fanUnit, err = NewSmartFanUnit(bcm2712SmartFanUnitDev)
if err != nil {
return fmt.Errorf("failed to create smart fan unit: %w", err)
}
} else {
log.FromContext(ctx).WithError(err).Info("no smart fan unit detected, assuming standard fan unit")
// Set GPIO 12 to PWM0_CHAN0 function
bcm.setGpioFuncsel(12, rp1Gpio12FuncselPwm)
// Initialize PWM0 channel 0 for fan control (25 kHz mark-space)
bcm.initFanPwm()
bcm.fanUnit = &standardFanUnitBcm2711{
GpioChip0: bcm.gpioChip0,
DisableRpmReporting: !bcm.opts.RpmReportingStandardFanUnit,
SetFanSpeedPwmFunc: func(speed uint8) error {
bcm.setFanSpeedPWM(speed)
return nil
},
}
}
return nil
}
// initFanPwm configures PWM0 channel 0 in mark-space mode at 25 kHz.
func (bcm *bcm2712) initFanPwm() {
ch := 0
// Disable channel 0
globalCtrl := bcm.pwmMem[rp1PwmGlobalCtrl/4]
globalCtrl &^= (1 << (rp1PwmGlobalChanEnBit + ch))
bcm.pwmMem[rp1PwmGlobalCtrl/4] = globalCtrl
time.Sleep(10 * time.Microsecond)
// Configure channel 0: mark-space mode, no FIFO, no invert
bcm.pwmMem[pwmChanRegIdx(ch, rp1PwmChanCtrlOff)] = uint32(rp1PwmModeMarkSpace) << rp1PwmChanCtrlModeBit
time.Sleep(10 * time.Microsecond)
// Set range (period) for 25 kHz
bcm.pwmMem[pwmChanRegIdx(ch, rp1PwmChanRangeOff)] = rp1FanPwmRange
time.Sleep(10 * time.Microsecond)
// Set initial duty to 0 (fan off)
bcm.pwmMem[pwmChanRegIdx(ch, rp1PwmChanDutyOff)] = 0
time.Sleep(10 * time.Microsecond)
// Phase = 0
bcm.pwmMem[pwmChanRegIdx(ch, rp1PwmChanPhaseOff)] = 0
time.Sleep(10 * time.Microsecond)
// Trigger update for channel 0
globalCtrl = bcm.pwmMem[rp1PwmGlobalCtrl/4]
globalCtrl |= (1 << rp1PwmGlobalSetUpdate)
bcm.pwmMem[rp1PwmGlobalCtrl/4] = globalCtrl
time.Sleep(10 * time.Microsecond)
// Enable channel 0
globalCtrl = bcm.pwmMem[rp1PwmGlobalCtrl/4]
globalCtrl |= (1 << (rp1PwmGlobalChanEnBit + ch))
bcm.pwmMem[rp1PwmGlobalCtrl/4] = globalCtrl
time.Sleep(10 * time.Microsecond)
}
func (bcm *bcm2712) 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 *bcm2712) handleEdgeButtonEdge(evt gpiod.LineEvent) {
select {
case bcm.edgeButtonDebounceChan <- struct{}{}:
go func() {
<-bcm.edgeButtonDebounceChan
time.Sleep(bcm2712DebounceInterval)
edgeButtonEventCount.Inc()
close(bcm.edgeButtonWatchChan)
bcm.edgeButtonWatchChan = make(chan struct{})
}()
default:
return
}
}
func (bcm *bcm2712) WaitForEdgeButtonPress(parentCtx context.Context) error {
ctx, cancel := context.WithCancel(parentCtx)
defer cancel()
fanUnitChan := make(chan struct{})
go func() {
err := bcm.fanUnit.WaitForButtonPress(ctx)
if err != nil && err != context.Canceled {
log.FromContext(ctx).WithError(err).Error("failed to wait for button press")
} else {
close(fanUnitChan)
}
}()
select {
case <-ctx.Done():
return ctx.Err()
case <-bcm.edgeButtonWatchChan:
return nil
case <-fanUnitChan:
return nil
}
}
func (bcm *bcm2712) GetFanRPM() (float64, error) {
rpm, err := bcm.fanUnit.FanSpeedRPM(context.TODO())
return float64(rpm), err
}
func (bcm *bcm2712) GetPowerStatus() (PowerStatus, error) {
val, err := bcm.poeLine.Value()
if err != nil {
return PowerPoeOrUsbC, err
}
if val > 0 {
powerStatus.WithLabelValues(fmt.Sprint(PowerPoe802at)).Set(1)
powerStatus.WithLabelValues(fmt.Sprint(PowerPoeOrUsbC)).Set(0)
return PowerPoe802at, nil
}
powerStatus.WithLabelValues(fmt.Sprint(PowerPoe802at)).Set(0)
powerStatus.WithLabelValues(fmt.Sprint(PowerPoeOrUsbC)).Set(1)
return PowerPoeOrUsbC, nil
}
// SetFanSpeed sets the fan speed in percent.
func (bcm *bcm2712) SetFanSpeed(speed uint8) error {
fanTargetPercent.Set(float64(speed))
return bcm.fanUnit.SetFanSpeedPercent(context.TODO(), speed)
}
// setFanSpeedPWM sets the fan PWM duty cycle using RP1 PWM0 channel 0 in mark-space mode.
func (bcm *bcm2712) setFanSpeedPWM(speed uint8) {
ch := 0
var duty uint32
if speed == 0 {
duty = 0
} else if speed <= 100 {
duty = uint32(float64(rp1FanPwmRange) * float64(speed) / 100.0)
} else {
duty = rp1FanPwmRange
}
// Update duty cycle
bcm.pwmMem[pwmChanRegIdx(ch, rp1PwmChanDutyOff)] = duty
// Trigger update
globalCtrl := bcm.pwmMem[rp1PwmGlobalCtrl/4]
globalCtrl |= (1 << rp1PwmGlobalSetUpdate)
bcm.pwmMem[rp1PwmGlobalCtrl/4] = globalCtrl
bcm.currFanSpeed = speed
}
func (bcm *bcm2712) SetStealthMode(enable bool) error {
if enable {
stealthModeEnabled.Set(1)
return bcm.stealthModeLine.SetValue(1)
}
stealthModeEnabled.Set(0)
return bcm.stealthModeLine.SetValue(0)
}
func (bcm *bcm2712) StealthModeActive() bool {
val, err := bcm.stealthModeLine.Value()
if err != nil {
return false
}
return val > 0
}
// SetLed sets the WS281x LED color via RP1 PWM0 channel 2 serializer mode.
// Unlike BCM2711 which shares PWM0 between fan and LEDs, RP1 has independent channels:
// channel 0 (GPIO 12) handles fan PWM, channel 2 (GPIO 18) handles WS281x LEDs.
func (bcm *bcm2712) SetLed(idx LedIndex, color led.Color) error {
if idx >= 2 {
return fmt.Errorf("invalid led index %d, supported: [0, 1]", idx)
}
// Update the fan unit LED if this is the edge LED
if idx == LedEdge {
if err := bcm.fanUnit.SetLed(context.TODO(), color); err != nil {
return err
}
}
bcm.leds[idx] = color
return bcm.updateLEDs()
}
// updateLEDs sends WS281x data via RP1 PWM0 channel 2 serializer mode.
// Uses the shared FIFO (channel 0 fan PWM uses mark-space mode without FIFO, so no conflict).
// The RP1 PWM clock is fixed at 50MHz, so we use 2 FIFO words per WS281x data bit
// at RANGE=32 to achieve ~1280ns per bit (within WS281x tolerance).
func (bcm *bcm2712) updateLEDs() error {
bcm.wrMutex.Lock()
defer bcm.wrMutex.Unlock()
ledColorChangeEventCount.Inc()
ch := rp1Ws281xChan
// Build complete FIFO data stream
data := bcm.buildWs281xStream()
// Set GPIO 18 to PWM0_CH2 function
bcm.setGpioFuncsel(18, rp1Gpio18FuncselPwm)
time.Sleep(10 * time.Microsecond)
// Disable channel 2
globalCtrl := bcm.pwmMem[rp1PwmGlobalCtrl/4]
globalCtrl &^= (1 << (rp1PwmGlobalChanEnBit + ch))
bcm.pwmMem[rp1PwmGlobalCtrl/4] = globalCtrl
time.Sleep(10 * time.Microsecond)
// Configure channel 2: serializer mode, use FIFO, SBIT=0 (low when idle)
bcm.pwmMem[pwmChanRegIdx(ch, rp1PwmChanCtrlOff)] =
(rp1PwmModeSerializer << rp1PwmChanCtrlModeBit) | (1 << rp1PwmChanCtrlUseFifoBit)
bcm.pwmMem[pwmChanRegIdx(ch, rp1PwmChanRangeOff)] = rp1Ws281xRange
bcm.pwmMem[pwmChanRegIdx(ch, rp1PwmChanPhaseOff)] = 0
time.Sleep(10 * time.Microsecond)
// Flush FIFO
bcm.pwmMem[rp1PwmFifoCtrl/4] = (1 << rp1PwmFifoFlushBit)
time.Sleep(10 * time.Microsecond)
// Trigger update for channel 2
globalCtrl = bcm.pwmMem[rp1PwmGlobalCtrl/4]
globalCtrl |= (1 << rp1PwmGlobalSetUpdate)
bcm.pwmMem[rp1PwmGlobalCtrl/4] = globalCtrl
time.Sleep(10 * time.Microsecond)
// Pre-fill FIFO before enabling channel
idx := 0
for idx < len(data) && uint32(idx) < rp1Ws281xFifoMax {
bcm.pwmMem[rp1PwmFifoPush/4] = data[idx]
idx++
}
// Lock OS thread for tight FIFO feeding
runtime.LockOSThread()
// Enable channel 2
globalCtrl = bcm.pwmMem[rp1PwmGlobalCtrl/4]
globalCtrl |= (1 << (rp1PwmGlobalChanEnBit + ch))
bcm.pwmMem[rp1PwmGlobalCtrl/4] = globalCtrl
// Push remaining data, polling FIFO level to avoid overflow
fifoLevelReg := rp1PwmFifoLevel / 4
fifoPushReg := rp1PwmFifoPush / 4
deadline := time.Now().Add(rp1Ws281xTimeout)
for idx < len(data) {
if time.Now().After(deadline) {
break
}
if bcm.pwmMem[fifoLevelReg] < rp1Ws281xFifoMax {
bcm.pwmMem[fifoPushReg] = data[idx]
idx++
}
}
// Wait for FIFO to drain
for bcm.pwmMem[fifoLevelReg] > 0 {
if time.Now().After(deadline) {
break
}
}
runtime.UnlockOSThread()
// Wait for last word to finish shifting out
time.Sleep(200 * time.Microsecond)
// Disable channel 2
globalCtrl = bcm.pwmMem[rp1PwmGlobalCtrl/4]
globalCtrl &^= (1 << (rp1PwmGlobalChanEnBit + ch))
bcm.pwmMem[rp1PwmGlobalCtrl/4] = globalCtrl
// Disconnect GPIO 18 from PWM to prevent residual noise on the data line
bcm.setGpioFuncsel(18, rp1GpioFuncselNull)
return nil
}
// buildWs281xStream constructs the complete FIFO word stream for both WS281x LEDs.
// Format: [reset padding] [top LED RGB] [edge LED RGB] [trailing zeros]
func (bcm *bcm2712) buildWs281xStream() []uint32 {
// Pre-allocate: 80 reset + 96 data (2 LEDs × 3 bytes × 16 words) + 2 trailing
words := make([]uint32, 0, rp1Ws281xResetWords+96+2)
// Reset padding (>50μs of low signal)
for i := 0; i < rp1Ws281xResetWords; i++ {
words = append(words, 0)
}
// Top LED (index 0) - RGB order (matches BCM2711 upstream)
words = appendByteWs281x(words, bcm.leds[0].Red)
words = appendByteWs281x(words, bcm.leds[0].Green)
words = appendByteWs281x(words, bcm.leds[0].Blue)
// Edge LED (index 1)
words = appendByteWs281x(words, bcm.leds[1].Red)
words = appendByteWs281x(words, bcm.leds[1].Green)
words = appendByteWs281x(words, bcm.leds[1].Blue)
// Trailing zeros for clean end-of-frame
words = append(words, 0, 0)
return words
}
// appendByteWs281x encodes one byte as 16 FIFO words (2 per data bit, MSB first) for RP1 WS281x.
func appendByteWs281x(words []uint32, b uint8) []uint32 {
for i := 7; i >= 0; i-- {
if (b>>uint(i))&1 == 0 {
words = append(words, rp1Ws281xBit0Word0, rp1Ws281xBit0Word1)
} else {
words = append(words, rp1Ws281xBit1Word0, rp1Ws281xBit1Word1)
}
}
return words
}
// GetTemperature returns the SoC temperature in degrees Celsius.
func (bcm *bcm2712) GetTemperature() (float64, error) {
f, err := os.Open(bcm2712ThermalZonePath)
if err != nil {
return -1, err
}
defer func() { _ = f.Close() }()
raw, err := io.ReadAll(f)
if err != nil {
return -1, err
}
cpuTemp, err := strconv.Atoi(strings.TrimSpace(string(raw)))
if err != nil {
return -1, err
}
temp := float64(cpuTemp) / 1000.0
socTemperature.Set(temp)
return temp, nil
}

178
pkg/hal/hal_rk3588.go Normal file
View File

@@ -0,0 +1,178 @@
//go:build linux && !tinygo
package hal
import (
"context"
"fmt"
"io"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/compute-blade-community/compute-blade-agent/pkg/hal/led"
"github.com/compute-blade-community/compute-blade-agent/pkg/log"
"go.uber.org/zap"
)
const (
rk3588ThermalZonePath = "/sys/class/thermal/thermal_zone0/temp"
rk3588PwmFanHwmonName = "pwmfan"
)
// rk3588 implements the ComputeBladeHal interface for the Rockchip RK3588 (Radxa CM5).
// Fan control uses the kernel's pwmfan driver via sysfs. GPIO-dependent features
// (button, stealth hardware, PoE detection, LEDs, tachometer) are stubbed until the
// Rockchip-to-B2B-connector pin mapping is determined.
type rk3588 struct {
opts ComputeBladeHalOpts
pwmPath string // e.g. /sys/class/hwmon/hwmon8/pwm1
pwmEnablePath string // e.g. /sys/class/hwmon/hwmon8/pwm1_enable
stealthMode bool
}
// Compile-time interface check
var _ ComputeBladeHal = &rk3588{}
func newRk3588Hal(ctx context.Context, opts ComputeBladeHalOpts) (*rk3588, error) {
logger := log.FromContext(ctx)
pwmPath, err := findHwmonPwm(rk3588PwmFanHwmonName)
if err != nil {
return nil, fmt.Errorf("failed to find pwmfan hwmon device: %w", err)
}
enablePath := pwmPath + "_enable"
// Set manual control mode (1 = manual PWM control)
if err := os.WriteFile(enablePath, []byte("1"), 0644); err != nil {
return nil, fmt.Errorf("failed to set pwm1_enable to manual mode: %w", err)
}
computeModule.WithLabelValues("radxa-cm5").Set(1)
logger.Info("starting hal setup", zap.String("hal", "rk3588"))
logger.Warn("GPIO pin mapping unknown for RK3588 B2B connector — button, stealth hardware, PoE detection, LEDs, and tachometer are stubbed")
return &rk3588{
opts: opts,
pwmPath: pwmPath,
pwmEnablePath: enablePath,
}, nil
}
func (rk *rk3588) Run(ctx context.Context) error {
fanUnit.WithLabelValues("sysfs").Set(1)
<-ctx.Done()
return ctx.Err()
}
func (rk *rk3588) Close() error {
return nil
}
// SetFanSpeed sets the fan speed via sysfs pwm1 (0-100% mapped to 0-255).
func (rk *rk3588) SetFanSpeed(speed uint8) error {
fanTargetPercent.Set(float64(speed))
var pwmVal uint8
if speed == 0 {
pwmVal = 0
} else if speed >= 100 {
pwmVal = 255
} else {
pwmVal = uint8(float64(speed) * 255.0 / 100.0)
}
return os.WriteFile(rk.pwmPath, []byte(strconv.Itoa(int(pwmVal))), 0644)
}
// GetFanRPM returns 0 — no tachometer GPIO is mapped on the RK3588.
func (rk *rk3588) GetFanRPM() (float64, error) {
return 0, nil
}
// GetTemperature returns the SoC temperature in degrees Celsius.
func (rk *rk3588) GetTemperature() (float64, error) {
f, err := os.Open(rk3588ThermalZonePath)
if err != nil {
return -1, err
}
defer func() { _ = f.Close() }()
raw, err := io.ReadAll(f)
if err != nil {
return -1, err
}
cpuTemp, err := strconv.Atoi(strings.TrimSpace(string(raw)))
if err != nil {
return -1, err
}
temp := float64(cpuTemp) / 1000.0
socTemperature.Set(temp)
return temp, nil
}
// SetStealthMode tracks stealth mode in software only (no GPIO mapped).
func (rk *rk3588) SetStealthMode(enabled bool) error {
rk.stealthMode = enabled
if enabled {
stealthModeEnabled.Set(1)
} else {
stealthModeEnabled.Set(0)
}
return nil
}
func (rk *rk3588) StealthModeActive() bool {
return rk.stealthMode
}
// SetLed is a no-op — WS281x LED GPIO pin mapping is unknown on RK3588.
func (rk *rk3588) SetLed(idx LedIndex, color led.Color) error {
ledColorChangeEventCount.Inc()
return nil
}
// GetPowerStatus returns PowerPoeOrUsbC as a safe default (no PoE detection GPIO mapped).
func (rk *rk3588) GetPowerStatus() (PowerStatus, error) {
powerStatus.WithLabelValues(fmt.Sprint(PowerPoe802at)).Set(0)
powerStatus.WithLabelValues(fmt.Sprint(PowerPoeOrUsbC)).Set(1)
return PowerPoeOrUsbC, nil
}
// WaitForEdgeButtonPress blocks until context cancellation (no button GPIO mapped).
func (rk *rk3588) WaitForEdgeButtonPress(ctx context.Context) error {
<-ctx.Done()
return ctx.Err()
}
// findHwmonPwm scans /sys/class/hwmon/hwmon*/name for a device matching the given name
// and returns the path to its pwm1 file.
func findHwmonPwm(name string) (string, error) {
matches, err := filepath.Glob("/sys/class/hwmon/hwmon*/name")
if err != nil {
return "", fmt.Errorf("failed to glob hwmon devices: %w", err)
}
for _, namePath := range matches {
raw, err := os.ReadFile(namePath)
if err != nil {
continue
}
if strings.TrimSpace(string(raw)) == name {
dir := filepath.Dir(namePath)
pwmPath := filepath.Join(dir, "pwm1")
if _, err := os.Stat(pwmPath); err != nil {
return "", fmt.Errorf("found %s hwmon at %s but pwm1 does not exist: %w", name, dir, err)
}
return pwmPath, nil
}
}
return "", fmt.Errorf("no hwmon device found with name %q", name)
}

39
pkg/hal/platform_linux.go Normal file
View File

@@ -0,0 +1,39 @@
//go:build linux && !tinygo
package hal
import (
"context"
"fmt"
"os"
"strings"
"github.com/compute-blade-community/compute-blade-agent/pkg/log"
"go.uber.org/zap"
)
const deviceTreeCompatiblePath = "/sys/firmware/devicetree/base/compatible"
// NewHal creates the appropriate HAL implementation based on the detected platform.
// It reads the device tree compatible string to determine whether the SoC is a
// BCM2711 (CM4/Pi 4) or BCM2712 (CM5/Pi 5).
func NewHal(ctx context.Context, opts ComputeBladeHalOpts) (ComputeBladeHal, error) {
compatible, err := os.ReadFile(deviceTreeCompatiblePath)
if err != nil {
return nil, fmt.Errorf("failed to read device tree compatible string: %w", err)
}
compatStr := string(compatible)
log.FromContext(ctx).Info("detected platform", zap.String("compatible", strings.ReplaceAll(compatStr, "\x00", ", ")))
switch {
case strings.Contains(compatStr, "bcm2712"):
return newBcm2712Hal(ctx, opts)
case strings.Contains(compatStr, "bcm2711"):
return newBcm2711Hal(ctx, opts)
case strings.Contains(compatStr, "rockchip,rk3588"):
return newRk3588Hal(ctx, opts)
default:
return nil, fmt.Errorf("unsupported platform: %s", strings.ReplaceAll(compatStr, "\x00", ", "))
}
}

115
scripts/find-tach-gpio.sh Executable file
View File

@@ -0,0 +1,115 @@
#!/bin/bash
# find-tach-gpio.sh - Safely probe GPIOs to find fan tachometer signal
# Run on Radxa node with fan at 100%: sudo ./find-tach-gpio.sh
#
# The tachometer generates 2 pulses per revolution. At 5000 RPM:
# 5000 RPM / 60 = 83.33 RPS * 2 pulses = ~167 Hz
# At 3000 RPM: ~100 Hz
# We look for any GPIO showing periodic edge events.
set -e
PROBE_DURATION=1 # seconds to monitor each GPIO
MIN_EVENTS=10 # minimum events to consider "active" (10 events in 1s = 600 RPM minimum)
DELAY_BETWEEN=0.5 # seconds between probes to let system settle
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo "=== Fan Tachometer GPIO Finder ==="
echo ""
# Check if running as root (needed for gpiomon)
if [ "$EUID" -ne 0 ]; then
echo -e "${RED}Please run as root (sudo)${NC}"
exit 1
fi
# Check fan speed
FAN_PWM=$(cat /sys/class/hwmon/hwmon8/pwm1 2>/dev/null || echo "unknown")
echo "Current fan PWM: $FAN_PWM (should be 255 for best detection)"
if [ "$FAN_PWM" != "255" ]; then
echo -e "${YELLOW}Setting fan to 100% for detection...${NC}"
echo 255 > /sys/class/hwmon/hwmon8/pwm1
sleep 2 # Let fan spin up
fi
echo ""
# Get list of gpiochips
CHIPS=$(ls /dev/gpiochip* | sed 's|/dev/||')
echo "Scanning GPIO chips: $CHIPS"
echo "Probe duration: ${PROBE_DURATION}s per line, minimum ${MIN_EVENTS} events to flag"
echo ""
FOUND_CANDIDATES=""
for chip in $CHIPS; do
# Get number of lines for this chip
NUM_LINES=$(gpioinfo $chip 2>/dev/null | wc -l)
NUM_LINES=$((NUM_LINES - 1)) # Subtract header line
if [ "$NUM_LINES" -le 0 ]; then
continue
fi
echo -e "${YELLOW}=== Scanning $chip ($NUM_LINES lines) ===${NC}"
for line in $(seq 0 $((NUM_LINES - 1))); do
# Check if line is already in use
LINE_INFO=$(gpioinfo $chip 2>/dev/null | grep "line *$line:" || true)
if echo "$LINE_INFO" | grep -q "\[used\]"; then
# Skip lines that are in use
continue
fi
# Probe the line
printf " Line %2d: " "$line"
# Run gpiomon with timeout, count events
EVENTS=$(timeout ${PROBE_DURATION}s gpiomon --num-events=100 $chip $line 2>&1 | wc -l || echo "0")
if [ "$EVENTS" -ge "$MIN_EVENTS" ]; then
# Calculate approximate frequency
FREQ=$((EVENTS / PROBE_DURATION))
RPM_ESTIMATE=$((FREQ * 60 / 2)) # 2 pulses per revolution
echo -e "${GREEN}ACTIVE! $EVENTS events (~${FREQ} Hz, ~${RPM_ESTIMATE} RPM)${NC}"
FOUND_CANDIDATES="$FOUND_CANDIDATES\n $chip line $line: $EVENTS events (~${RPM_ESTIMATE} RPM)"
elif [ "$EVENTS" -gt 0 ]; then
echo "$EVENTS events (noise?)"
else
echo "no events"
fi
# Small delay to let system settle
sleep $DELAY_BETWEEN
done
echo ""
done
echo "=== Scan Complete ==="
if [ -n "$FOUND_CANDIDATES" ]; then
echo -e "${GREEN}Candidate tachometer GPIOs found:${NC}"
echo -e "$FOUND_CANDIDATES"
echo ""
echo "To verify, try monitoring the candidate with varying fan speeds:"
echo " gpiomon --num-events=50 <chip> <line>"
echo ""
echo "Then add to device tree or HAL configuration."
else
echo -e "${YELLOW}No active GPIOs found.${NC}"
echo "Possible reasons:"
echo " - Fan tachometer not connected on this carrier board"
echo " - Tachometer uses a different interface (I2C, ADC, etc.)"
echo " - Fan doesn't have tachometer wire connected"
fi
# Restore fan to auto if we changed it
if [ "$FAN_PWM" != "255" ] && [ "$FAN_PWM" != "unknown" ]; then
echo ""
echo "Restoring fan PWM to $FAN_PWM"
echo "$FAN_PWM" > /sys/class/hwmon/hwmon8/pwm1
fi