https://github.com/westphae/go-iio
Go library for reading Linux Industrial I/O (IIO) sensors — device-tree-overlay sensors like BMP280 and ICM20948 — via sysfs. Pure Go, no cgo.
https://github.com/westphae/go-iio
bmp280 device-tree golang iio linux raspberry-pi sensors sysfs
Last synced: 1 day ago
JSON representation
Go library for reading Linux Industrial I/O (IIO) sensors — device-tree-overlay sensors like BMP280 and ICM20948 — via sysfs. Pure Go, no cgo.
- Host: GitHub
- URL: https://github.com/westphae/go-iio
- Owner: westphae
- License: mit
- Created: 2026-05-12T12:40:11.000Z (about 2 months ago)
- Default Branch: master
- Last Pushed: 2026-05-15T21:02:04.000Z (about 1 month ago)
- Last Synced: 2026-05-15T23:52:50.143Z (about 1 month ago)
- Topics: bmp280, device-tree, golang, iio, linux, raspberry-pi, sensors, sysfs
- Language: Go
- Size: 54.7 KB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# go-iio
[](https://pkg.go.dev/github.com/westphae/go-iio)
[](https://github.com/westphae/go-iio/actions/workflows/ci.yml)
[](https://goreportcard.com/report/github.com/westphae/go-iio)
[](LICENSE)
A small, idiomatic Go library for reading Linux Industrial I/O (IIO) sensors
— the ones that show up under `/sys/bus/iio/devices/` after a device tree
overlay loads. Polled and buffered captures, dynamic channel discovery, no
cgo.
```go
import "github.com/westphae/go-iio"
dev, _ := iio.Open("bmp280")
defer dev.Close()
t, _ := dev.ReadFloat("temp") // °C
p, _ := dev.ReadFloat("pressure") // kPa
```
Buffered capture at a fixed rate (BMP280 has no hardware data-ready, so the
library drives it from a kernel hrtimer trigger):
```go
trig, _ := iio.EnsureHRTimer("mytrig", 10) // 10 Hz; needs CAP_SYS_ADMIN
defer trig.Remove()
buf, _ := dev.Buffer(iio.BufferOptions{
Channels: []string{"pressure", "temp", "timestamp"},
Length: 16,
Trigger: trig.Name(),
})
defer buf.Close()
recs := make([]iio.Record, 16)
for {
n, _ := buf.Read(ctx, recs)
for i := 0; i < n; i++ { /* recs[i].Time, recs[i].Values["temp"], ... */ }
}
```
See `examples/` for runnable programs, and `CLAUDE.md` for the design notes.
## Sensor wrappers
The generic `iio` API works for any kernel-supported sensor, but the
`bmp280`, `icm20948`, and `mmc5983ma` subpackages bundle the channel names,
scale writes, trigger setup, and decode logic specific to those chips so
the calling code is just `Open / Read / Stream`. All three wrappers expose
`Device()` for the underlying `*iio.Device` when you need an attribute the
wrapper doesn't cover.
### BMP280
Bosch BMP280 pressure/temperature sensor. Wraps the mainline `bmp280`
kernel driver. Samples are `°C` and `kPa`.
```go
import "github.com/westphae/go-iio/bmp280"
dev, err := bmp280.Open(
bmp280.WithOversampling(2, 16), // temp x2, pressure x16
)
if err != nil { /* ... */ }
defer dev.Close()
// Polled — one sample per call, two sysfs reads each:
s, _ := dev.Read()
fmt.Println(s.TempC, s.PressKPa)
```
Buffered capture (the BMP280 has no hardware data-ready, so the wrapper
creates an hrtimer trigger via configfs — needs `CAP_SYS_ADMIN`, and
`iio-trig-hrtimer` must be loaded):
```go
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ch, err := dev.Stream(ctx, bmp280.StreamOptions{
FrequencyHz: 10, // required, > 0
TriggerName: "", // optional, defaults to "goiio-bmp280"
BufferLength: 16, // kernel ring depth in samples
})
if err != nil { /* ... */ }
for s := range ch {
fmt.Printf("%s %.2f °C %.3f kPa\n", s.Time.Format(time.RFC3339Nano), s.TempC, s.PressKPa)
}
```
Options:
- `WithPath(p)` — open a specific sysfs path instead of looking up by name
(useful if multiple BMP280s are present).
- `WithOversampling(temp, pressure)` — writes `in_temp_oversampling_ratio`
and `in_pressure_oversampling_ratio`. Valid: `1, 2, 4, 8, 16`.
### ICM-20948
InvenSense / TDK ICM-20948 9-axis IMU (3-axis accel + 3-axis gyro + AK09916
magnetometer + die temperature). Targets the out-of-tree
`github.com/westphae/icm20948-mod` driver or the mainline `inv_icm20948`
driver — both expose the same standard IIO channel naming. Samples are SI:
m/s² (accel), rad/s (gyro), µT (mag — converted from the kernel's Gauss),
°C (temp).
```go
import "github.com/westphae/go-iio/icm20948"
dev, err := icm20948.Open(
icm20948.WithAccelScale(4), // ±4 G
icm20948.WithGyroScale(500), // ±500 dps
icm20948.WithAccelDLPFHz(50), // snap to nearest available cutoff
icm20948.WithGyroDLPFHz(50),
)
if err != nil { /* ... */ }
defer dev.Close()
// Polled — one Sample per call, ten sysfs reads:
s, _ := dev.Read()
fmt.Printf("a=(%.2f,%.2f,%.2f) g=(%.3f,%.3f,%.3f) m=(%.1f,%.1f,%.1f) T=%.1f\n",
s.AccelX, s.AccelY, s.AccelZ,
s.GyroX, s.GyroY, s.GyroZ,
s.MagX, s.MagY, s.MagZ,
s.TempC,
)
```
Buffered capture (same hrtimer mechanics as BMP280):
```go
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ch, err := dev.Stream(ctx, icm20948.StreamOptions{
FrequencyHz: 100, // accel/gyro pace; AK09916 mag is fixed at 100 Hz in the driver
BufferLength: 32,
})
if err != nil { /* ... */ }
for s := range ch {
// s.Time, s.AccelX/Y/Z, s.GyroX/Y/Z, s.MagX/Y/Z, s.TempC
}
```
Options:
- `WithPath(p)` — open a specific sysfs path instead of looking up by name.
- `WithAccelScale(g)` — `2 / 4 / 8 / 16`. Writes `in_accel_scale` with the
exact SI string the driver expects.
- `WithGyroScale(dps)` — `250 / 500 / 1000 / 2000`. Writes `in_anglvel_scale`.
- `WithAccelDLPFHz(hz)` / `WithGyroDLPFHz(hz)` — on-chip digital low-pass
cutoff. The driver only accepts values from
`in__filter_low_pass_3db_frequency_available`; the wrapper snaps to
the nearest available value.
Magnetometer overflow: the AK09916 saturates at ±4912 µT. The wrapper
exposes the sticky `in_magn_overrange` flag so callers can detect a clipped
axis without scanning every sample:
```go
if over, _ := dev.Overrange(); over {
// at least one sample since the last clear hit the chip's range
_ = dev.ClearOverrange()
}
```
Note on rates: the magnetometer's internal conversion runs at a fixed
100 Hz in the kernel driver, so at trigger rates above 100 Hz consecutive
samples will repeat the same mag values until the next conversion lands.
### MMC5983MA
MEMSIC MMC5983MA 3-axis AMR magnetometer (±8 G, 18-bit, on-chip temperature
sensor). Targets the out-of-tree `github.com/westphae/mmc5983ma-mod`
kernel driver. Samples are µT (mag — converted from the kernel's Gauss)
and °C (temp).
```go
import "github.com/westphae/go-iio/mmc5983ma"
dev, err := mmc5983ma.Open(
mmc5983ma.WithSamplingFrequencyHz(100), // continuous-mode ODR
mmc5983ma.WithBandwidthHz(200), // measurement bandwidth
)
if err != nil { /* ... */ }
defer dev.Close()
// Polled — one Sample per call, four sysfs reads (mag x/y/z + temp):
s, _ := dev.Read()
fmt.Printf("mag: %.1f %.1f %.1f µT %.2f °C\n",
s.MagX, s.MagY, s.MagZ, s.TempC)
```
Buffered capture (same hrtimer mechanics as BMP280/ICM-20948):
```go
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ch, err := dev.Stream(ctx, mmc5983ma.StreamOptions{
FrequencyHz: 100,
BufferLength: 32,
})
if err != nil { /* ... */ }
for s := range ch {
// s.Time, s.MagX/Y/Z — TempC is not in the buffered scan; poll Read for it.
}
```
Options:
- `WithPath(p)` — open a specific sysfs path instead of looking up by name.
- `WithSamplingFrequencyHz(hz)` — `1 / 10 / 20 / 50 / 100 / 200 / 1000`.
The driver auto-bumps `BandwidthHz` if required by the chip's
ODR-vs-BW constraints (200 Hz → BW≥200, 1000 Hz → BW=800).
- `WithBandwidthHz(hz)` — `100 / 200 / 400 / 800`. Higher BW = shorter
measurement window = noisier readings but faster max ODR.
- `WithPeriodicSet(samples)` — supplementary periodic-SET cadence on top
of Auto SR (which is always on). `0` disables. Useful only in extreme
thermal environments; leave at the default otherwise.
AMR-specific helpers:
```go
// Recover after a strong-field event:
_ = dev.SetPulse() // 500 ns SET pulse through the chip's coil
// Null the bridge offset (best in a known-stable field — Helmholtz coil
// or magnetic shield). Updates the driver's software calibbias so
// subsequent Read/Stream values come back with offset cancelled.
_ = dev.AutoNullCalibBias()
x, y, z, _ := dev.CalibBias() // inspect the new offsets
// Confirm the chip is responsive (St_enp / St_enm coils):
_ = dev.RunSelftest(true) // positive coil
dx, dy, dz, _ := dev.SelftestDelta() // delta vs baseline in raw LSB
_ = dev.ClearSelftest()
```
Note on temperature: the chip can only do mag OR temp at a time, so the
buffered scan layout includes the three mag axes plus timestamp but
*not* temperature. `Sample.TempC` is zero in `Stream` samples; use
`Read()` (which briefly pauses the mag stream for a one-shot temp
measurement) when you need temperature.
## Status
v0: pure-Go sysfs backend, BMP280 polled and buffered captures, ICM-20948
and MMC5983MA convenience wrappers. A `backend/libiio` (cgo) slot is
reserved for remote sensors over `iiod` but is not yet implemented.