update BRC pulse handling

This commit is contained in:
legop3
2025-11-11 19:48:50 -05:00
parent c89e9cdfd3
commit 3bc2921f25
10 changed files with 49 additions and 41 deletions
+1 -1
View File
@@ -43,4 +43,4 @@ sudo ./pi/install_roverd.sh --mediamtx
Then point each rover's `/etc/roverd.yaml` at `ws://<server>:8080/rover`, enable the sensor stream from the UI, and drive with WASD. Then point each rover's `/etc/roverd.yaml` at `ws://<server>:8080/rover`, enable the sensor stream from the UI, and drive with WASD.
Use the “Restart Camera” button if you enable media management so roverd can bounce the mediamtx service remotely. Use the “Restart Camera” button if you enable media management so roverd can bounce the mediamtx service remotely.
Heads-up: the BRC pulser currently hits `/sys/class/gpio/export`, so the service needs root privileges on Raspberry Pi OS. Until its switched to `libgpiod`, set `brc.gpioPin: -1` if you prefer to run unprivileged (the Create will then nap unless something else keeps it awake). Heads-up: the BRC pulser now uses libgpiod; make sure the `roverd` service account is in the `gpio` group (or otherwise allowed to access `/dev/gpiochip*`) and set `brc.gpioChip` if your hardware exposes a different chip name.
Vendored
BIN
View File
Binary file not shown.
+3 -3
View File
@@ -62,9 +62,9 @@ If the script installs the sample config, it will remind you to edit `/etc/rover
sudo systemctl enable --now roverd.service sudo systemctl enable --now roverd.service
``` ```
`roverd` requires access to `/dev/ttyAMA0` and `/sys/class/gpio`; keeping it under its own user ensures the rest of the system stays isolated. `roverd` requires access to `/dev/ttyAMA0` and `/dev/gpiochip*`; keeping it under its own user ensures the rest of the system stays isolated—just make sure the account belongs to the `dialout` and `gpio` groups so it can reach the UART and libgpiod.
`mediamtx` needs read access to the camera devices (`/dev/media*`, `/dev/video*`), so the install script adds its service account to the `video` group; if you created the user manually, make sure it belongs to `video`. `mediamtx` needs read access to the camera devices (`/dev/media*`, `/dev/video*`), so the install script adds its service account to the `video` group; if you created the user manually, make sure it belongs to `video`.
**BRC note:** writing to `/sys/class/gpio/export` typically requires root on Raspberry Pi OS. Until the pulser moves to `libgpiod`, either run the service as root or set `brc.gpioPin: -1` (rover may eventually sleep). **BRC note:** configure `brc.gpioPin` (and `brc.gpioChip` if youre not using `gpiochip0`) and ensure the `roverd` user has permission to toggle that line—no root privileges are required anymore.
If you set `media.manage: true` in `/etc/roverd.yaml`, make sure the `roverd` service account can invoke `systemctl <action> <media.service>` (either run the unit as root or grant sudo privileges for that command). If you set `media.manage: true` in `/etc/roverd.yaml`, make sure the `roverd` service account can invoke `systemctl <action> <media.service>` (either run the unit as root or grant sudo privileges for that command).
## Configuring mediaMTX ## Configuring mediaMTX
+24 -36
View File
@@ -2,36 +2,42 @@ package roverd
import ( import (
"context" "context"
"fmt"
"log" "log"
"os"
"path/filepath"
"strconv"
"time" "time"
gpiocdev "github.com/warthog618/go-gpiocdev"
) )
type BRCPulser struct { type BRCPulser struct {
cfg BRCConfig cfg BRCConfig
logger *log.Logger logger *log.Logger
line *gpiocdev.Line
} }
func NewBRCPulser(cfg BRCConfig, logger *log.Logger) (*BRCPulser, error) { func NewBRCPulser(cfg BRCConfig, logger *log.Logger) (*BRCPulser, error) {
if err := exportGPIO(cfg.GPIOPin); err != nil { chip := cfg.GPIOChip
return nil, err if chip == "" {
chip = "gpiochip0"
} }
if err := writeGPIO(cfg.GPIOPin, "direction", []byte("out\n")); err != nil {
return nil, err line, err := gpiocdev.RequestLine(
} chip,
if err := writeGPIO(cfg.GPIOPin, "value", []byte("1\n")); err != nil { cfg.GPIOPin,
gpiocdev.AsOutput(1),
gpiocdev.WithConsumer("roverd-brc"),
)
if err != nil {
return nil, err return nil, err
} }
return &BRCPulser{cfg: cfg, logger: logger}, nil return &BRCPulser{cfg: cfg, logger: logger, line: line}, nil
} }
func (b *BRCPulser) Close() { func (b *BRCPulser) Close() {
_ = writeGPIO(b.cfg.GPIOPin, "value", []byte("1\n")) if b.line != nil {
_ = unexportGPIO(b.cfg.GPIOPin) _ = b.line.SetValue(1)
b.line.Close()
}
} }
func (b *BRCPulser) Start(ctx context.Context) { func (b *BRCPulser) Start(ctx context.Context) {
@@ -51,33 +57,15 @@ func (b *BRCPulser) Start(ctx context.Context) {
} }
func (b *BRCPulser) pulseOnce() { func (b *BRCPulser) pulseOnce() {
if err := writeGPIO(b.cfg.GPIOPin, "value", []byte("0\n")); err != nil { if b.line == nil {
return
}
if err := b.line.SetValue(0); err != nil {
b.logger.Printf("brc pulse low: %v", err) b.logger.Printf("brc pulse low: %v", err)
return return
} }
time.Sleep(b.cfg.PulseWidth.Duration) time.Sleep(b.cfg.PulseWidth.Duration)
if err := writeGPIO(b.cfg.GPIOPin, "value", []byte("1\n")); err != nil { if err := b.line.SetValue(1); err != nil {
b.logger.Printf("brc pulse high: %v", err) b.logger.Printf("brc pulse high: %v", err)
} }
} }
func exportGPIO(pin int) error {
err := os.WriteFile("/sys/class/gpio/export", []byte(strconv.Itoa(pin)), 0o644)
if err != nil && !os.IsExist(err) {
return err
}
return nil
}
func unexportGPIO(pin int) error {
err := os.WriteFile("/sys/class/gpio/unexport", []byte(strconv.Itoa(pin)), 0o644)
if err != nil && !os.IsNotExist(err) {
return err
}
return nil
}
func writeGPIO(pin int, field string, data []byte) error {
path := filepath.Join("/sys/class/gpio", fmt.Sprintf("gpio%d", pin), field)
return os.WriteFile(path, data, 0o644)
}
+6 -1
View File
@@ -37,6 +37,7 @@ func (d Duration) MarshalYAML() (interface{}, error) {
type BRCConfig struct { type BRCConfig struct {
GPIOPin int `yaml:"gpioPin"` GPIOPin int `yaml:"gpioPin"`
GPIOChip string `yaml:"gpioChip"`
PulseEvery Duration `yaml:"pulseEvery"` PulseEvery Duration `yaml:"pulseEvery"`
PulseWidth Duration `yaml:"pulseWidth"` PulseWidth Duration `yaml:"pulseWidth"`
} }
@@ -78,7 +79,8 @@ func LoadConfig(path string) (*Config, error) {
cfg := Config{ cfg := Config{
MaxWheelMMs: 500, MaxWheelMMs: 500,
BRC: BRCConfig{ BRC: BRCConfig{
GPIOPin: -1, GPIOPin: -1,
GPIOChip: "gpiochip0",
PulseEvery: Duration{ PulseEvery: Duration{
Duration: time.Minute, Duration: time.Minute,
}, },
@@ -108,6 +110,9 @@ func LoadConfig(path string) (*Config, error) {
if cfg.MaxWheelMMs <= 0 || cfg.MaxWheelMMs > 500 { if cfg.MaxWheelMMs <= 0 || cfg.MaxWheelMMs > 500 {
return nil, fmt.Errorf("maxWheelSpeed must be 1-500, got %d", cfg.MaxWheelMMs) return nil, fmt.Errorf("maxWheelSpeed must be 1-500, got %d", cfg.MaxWheelMMs)
} }
if cfg.BRC.GPIOChip == "" {
cfg.BRC.GPIOChip = "gpiochip0"
}
if cfg.Media.Manage && cfg.Media.Service == "" { if cfg.Media.Manage && cfg.Media.Service == "" {
return nil, errors.New("media.manage requires media.service") return nil, errors.New("media.manage requires media.service")
} }
+1
View File
@@ -4,6 +4,7 @@ go 1.25.4
require ( require (
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07
github.com/warthog618/go-gpiocdev v0.9.1
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
nhooyr.io/websocket v1.8.17 nhooyr.io/websocket v1.8.17
) )
+12
View File
@@ -1,5 +1,17 @@
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/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 h1:UyzmZLoiDWMRywV4DUYb9Fbt8uiOSooupjTq10vpvnU= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 h1:UyzmZLoiDWMRywV4DUYb9Fbt8uiOSooupjTq10vpvnU=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
github.com/warthog618/go-gpiocdev v0.9.1 h1:pwHPaqjJfhCipIQl78V+O3l9OKHivdRDdmgXYbmhuCI=
github.com/warthog618/go-gpiocdev v0.9.1/go.mod h1:dN3e3t/S2aSNC+hgigGE/dBW8jE1ONk9bDSEYfoPyl8=
github.com/warthog618/go-gpiosim v0.1.1 h1:MRAEv+T+itmw+3GeIGpQJBfanUVyg0l3JCTwHtwdre4=
github.com/warthog618/go-gpiosim v0.1.1/go.mod h1:YXsnB+I9jdCMY4YAlMSRrlts25ltjmuIsrnoUrBLdqU=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= 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.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
BIN
View File
Binary file not shown.
+1
View File
@@ -6,6 +6,7 @@ serial:
baud: 115200 baud: 115200
brc: brc:
gpioPin: 17 gpioPin: 17
gpioChip: gpiochip0
pulseEvery: 1m pulseEvery: 1m
pulseWidth: 1s pulseWidth: 1s
battery: battery:
+1
View File
@@ -6,6 +6,7 @@ serial:
baud: 115200 baud: 115200
brc: brc:
gpioPin: 17 gpioPin: 17
gpioChip: gpiochip0
pulseEvery: 1m pulseEvery: 1m
pulseWidth: 1s pulseWidth: 1s
battery: battery: