diff --git a/README.md b/README.md index 5e33aca1..4e7b20ee 100644 --- a/README.md +++ b/README.md @@ -43,4 +43,4 @@ sudo ./pi/install_roverd.sh --mediamtx Then point each rover's `/etc/roverd.yaml` at `ws://: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 it’s 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. diff --git a/dist/roverd b/dist/roverd index 117f8af1..9a8c6690 100755 Binary files a/dist/roverd and b/dist/roverd differ diff --git a/docs/pi-deployment.md b/docs/pi-deployment.md index 7128ddef..6d424d4d 100644 --- a/docs/pi-deployment.md +++ b/docs/pi-deployment.md @@ -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 you’re 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 ` (either run the unit as root or grant sudo privileges for that command). ## Configuring mediaMTX diff --git a/pi/roverd/brc.go b/pi/roverd/brc.go index 8dda06b5..7d7fb755 100644 --- a/pi/roverd/brc.go +++ b/pi/roverd/brc.go @@ -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) -} diff --git a/pi/roverd/config.go b/pi/roverd/config.go index 5e88d4f1..1a236b08 100644 --- a/pi/roverd/config.go +++ b/pi/roverd/config.go @@ -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") } diff --git a/pi/roverd/go.mod b/pi/roverd/go.mod index 38af9d90..a69b6c2a 100644 --- a/pi/roverd/go.mod +++ b/pi/roverd/go.mod @@ -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 ) diff --git a/pi/roverd/go.sum b/pi/roverd/go.sum index b939c5d1..819bfeeb 100644 --- a/pi/roverd/go.sum +++ b/pi/roverd/go.sum @@ -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= diff --git a/pi/roverd/roverd b/pi/roverd/roverd deleted file mode 100755 index 5b7e6c21..00000000 Binary files a/pi/roverd/roverd and /dev/null differ diff --git a/pi/roverd/roverd.sample.yaml b/pi/roverd/roverd.sample.yaml index defd0586..975b5105 100644 --- a/pi/roverd/roverd.sample.yaml +++ b/pi/roverd/roverd.sample.yaml @@ -6,6 +6,7 @@ serial: baud: 115200 brc: gpioPin: 17 + gpioChip: gpiochip0 pulseEvery: 1m pulseWidth: 1s battery: diff --git a/pi/roverd/roverd.yaml b/pi/roverd/roverd.yaml index defd0586..975b5105 100644 --- a/pi/roverd/roverd.yaml +++ b/pi/roverd/roverd.yaml @@ -6,6 +6,7 @@ serial: baud: 115200 brc: gpioPin: 17 + gpioChip: gpiochip0 pulseEvery: 1m pulseWidth: 1s battery: