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.
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
```
`roverd` requires access to `/dev/ttyAMA0` and `/sys/class/gpio`; keeping it under its own user ensures the rest of the system stays isolated.
`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).
`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`.
**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).
## Configuring mediaMTX
+24 -36
View File
@@ -2,36 +2,42 @@ package roverd
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"strconv"
"time"
gpiocdev "github.com/warthog618/go-gpiocdev"
)
type BRCPulser struct {
cfg BRCConfig
logger *log.Logger
line *gpiocdev.Line
}
func NewBRCPulser(cfg BRCConfig, logger *log.Logger) (*BRCPulser, error) {
if err := exportGPIO(cfg.GPIOPin); err != nil {
return nil, err
chip := cfg.GPIOChip
if chip == "" {
chip = "gpiochip0"
}
if err := writeGPIO(cfg.GPIOPin, "direction", []byte("out\n")); err != nil {
return nil, err
}
if err := writeGPIO(cfg.GPIOPin, "value", []byte("1\n")); err != nil {
line, err := gpiocdev.RequestLine(
chip,
cfg.GPIOPin,
gpiocdev.AsOutput(1),
gpiocdev.WithConsumer("roverd-brc"),
)
if err != nil {
return nil, err
}
return &BRCPulser{cfg: cfg, logger: logger}, nil
return &BRCPulser{cfg: cfg, logger: logger, line: line}, nil
}
func (b *BRCPulser) Close() {
_ = writeGPIO(b.cfg.GPIOPin, "value", []byte("1\n"))
_ = unexportGPIO(b.cfg.GPIOPin)
if b.line != nil {
_ = b.line.SetValue(1)
b.line.Close()
}
}
func (b *BRCPulser) Start(ctx context.Context) {
@@ -51,33 +57,15 @@ func (b *BRCPulser) Start(ctx context.Context) {
}
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)
return
}
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)
}
}
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 {
GPIOPin int `yaml:"gpioPin"`
GPIOChip string `yaml:"gpioChip"`
PulseEvery Duration `yaml:"pulseEvery"`
PulseWidth Duration `yaml:"pulseWidth"`
}
@@ -78,7 +79,8 @@ func LoadConfig(path string) (*Config, error) {
cfg := Config{
MaxWheelMMs: 500,
BRC: BRCConfig{
GPIOPin: -1,
GPIOPin: -1,
GPIOChip: "gpiochip0",
PulseEvery: Duration{
Duration: time.Minute,
},
@@ -108,6 +110,9 @@ func LoadConfig(path string) (*Config, error) {
if cfg.MaxWheelMMs <= 0 || cfg.MaxWheelMMs > 500 {
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 == "" {
return nil, errors.New("media.manage requires media.service")
}
+1
View File
@@ -4,6 +4,7 @@ go 1.25.4
require (
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07
github.com/warthog618/go-gpiocdev v0.9.1
gopkg.in/yaml.v3 v3.0.1
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/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/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
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
brc:
gpioPin: 17
gpioChip: gpiochip0
pulseEvery: 1m
pulseWidth: 1s
battery:
+1
View File
@@ -6,6 +6,7 @@ serial:
baud: 115200
brc:
gpioPin: 17
gpioChip: gpiochip0
pulseEvery: 1m
pulseWidth: 1s
battery: