├── pi5 ├── mock-device-tree │ └── proc │ │ └── device-tree │ │ ├── #size-cells │ │ ├── #address-cells │ │ ├── axi │ │ ├── #size-cells │ │ ├── #address-cells │ │ ├── pcie@120000 │ │ │ ├── #address-cells │ │ │ ├── #size-cells │ │ │ ├── rp1 │ │ │ │ ├── #size-cells │ │ │ │ ├── #address-cells │ │ │ │ ├── gpiomem@d0000 │ │ │ │ │ ├── name │ │ │ │ │ ├── chardev-name │ │ │ │ │ ├── compatible │ │ │ │ │ └── reg │ │ │ │ ├── ranges │ │ │ │ └── gpio@d0000 │ │ │ │ │ └── reg │ │ │ ├── reg │ │ │ └── ranges │ │ └── ranges │ │ └── aliases │ │ └── gpio0 ├── board_test.go ├── data.go └── board.go ├── rpi ├── pi.h ├── pi.c ├── analog_reader.go ├── interrupts.go ├── gpio.go ├── board_test.go └── board.go ├── tools.go ├── .canon.yaml ├── .gitignore ├── utils ├── system_helpers.go ├── config.go ├── system_helpers_test.go ├── broadcom.go ├── digital_interrupts_test.go ├── digital_interrupts.go ├── file_helpers.go ├── file_helpers_test.go └── errors.go ├── main.go ├── .github └── workflows │ ├── deploy.yml │ └── test.yml ├── run.sh ├── docker └── Dockerfile ├── rpi-servo ├── config.go ├── rpiservo_helpers.go ├── rpiservo.go └── rpiservo_test.go ├── tests ├── rpiservo_external_test.go └── rpi_external_test.go ├── meta.json ├── etc └── .golangci.yaml ├── Makefile ├── LICENSE ├── README.md └── go.mod /pi5/mock-device-tree/proc/device-tree/#size-cells: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /pi5/mock-device-tree/proc/device-tree/#address-cells: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /pi5/mock-device-tree/proc/device-tree/axi/#size-cells: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /pi5/mock-device-tree/proc/device-tree/axi/#address-cells: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /pi5/mock-device-tree/proc/device-tree/axi/pcie@120000/#address-cells: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /pi5/mock-device-tree/proc/device-tree/axi/pcie@120000/#size-cells: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /pi5/mock-device-tree/proc/device-tree/axi/pcie@120000/rp1/#size-cells: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /pi5/mock-device-tree/proc/device-tree/axi/pcie@120000/rp1/#address-cells: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /pi5/mock-device-tree/proc/device-tree/aliases/gpio0: -------------------------------------------------------------------------------- 1 | /axi/pcie@120000/rp1/gpio@d0000 -------------------------------------------------------------------------------- /pi5/mock-device-tree/proc/device-tree/axi/pcie@120000/rp1/gpiomem@d0000/name: -------------------------------------------------------------------------------- 1 | gpiomem -------------------------------------------------------------------------------- /pi5/mock-device-tree/proc/device-tree/axi/pcie@120000/rp1/gpiomem@d0000/chardev-name: -------------------------------------------------------------------------------- 1 | gpiomem0 -------------------------------------------------------------------------------- /pi5/mock-device-tree/proc/device-tree/axi/pcie@120000/rp1/gpiomem@d0000/compatible: -------------------------------------------------------------------------------- 1 | raspberrypi,gpiomem -------------------------------------------------------------------------------- /pi5/mock-device-tree/proc/device-tree/axi/ranges: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /pi5/mock-device-tree/proc/device-tree/axi/pcie@120000/reg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnwalicki/viam-raspberry-pi/main/pi5/mock-device-tree/proc/device-tree/axi/pcie@120000/reg -------------------------------------------------------------------------------- /pi5/mock-device-tree/proc/device-tree/axi/pcie@120000/ranges: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnwalicki/viam-raspberry-pi/main/pi5/mock-device-tree/proc/device-tree/axi/pcie@120000/ranges -------------------------------------------------------------------------------- /pi5/mock-device-tree/proc/device-tree/axi/pcie@120000/rp1/ranges: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnwalicki/viam-raspberry-pi/main/pi5/mock-device-tree/proc/device-tree/axi/pcie@120000/rp1/ranges -------------------------------------------------------------------------------- /pi5/mock-device-tree/proc/device-tree/axi/pcie@120000/rp1/gpio@d0000/reg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnwalicki/viam-raspberry-pi/main/pi5/mock-device-tree/proc/device-tree/axi/pcie@120000/rp1/gpio@d0000/reg -------------------------------------------------------------------------------- /pi5/mock-device-tree/proc/device-tree/axi/pcie@120000/rp1/gpiomem@d0000/reg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnwalicki/viam-raspberry-pi/main/pi5/mock-device-tree/proc/device-tree/axi/pcie@120000/rp1/gpiomem@d0000/reg -------------------------------------------------------------------------------- /rpi/pi.h: -------------------------------------------------------------------------------- 1 | /* 2 | pi.h: Header file for pi.c 3 | */ 4 | #pragma once 5 | 6 | // interruptCallback calls through to the go linked interrupt callback. 7 | int setupInterrupt(int pi, int gpio); 8 | int setPullDown(int pi, int gpio); 9 | int setPullUp(int pi, int gpio); 10 | int setPullNone(int pi, int gpio); 11 | int teardownInterrupt(unsigned int callback_id); 12 | -------------------------------------------------------------------------------- /tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | // +build tools 3 | 4 | package tools 5 | 6 | import ( 7 | _ "github.com/edaniels/golinters/cmd/combined" 8 | _ "github.com/golangci/golangci-lint/cmd/golangci-lint" 9 | _ "github.com/rhysd/actionlint/cmd/actionlint" 10 | ) 11 | 12 | // This file is used for build-time dependencies only and does not contribute to the actual application. 13 | -------------------------------------------------------------------------------- /.canon.yaml: -------------------------------------------------------------------------------- 1 | # This file provides project-level configuration for the canon dev environment utility. https://github.com/viamrobotics/canon 2 | pi: 3 | default: true 4 | arch: arm64 5 | image_arm64: ghcr.io/viam-modules/raspberry-pi:arm64 6 | image_arm: ghcr.io/viam-modules/raspberry-pi:arm 7 | minimum_date: 2024-08-21T00:00:00.0Z 8 | update_interval: 168h0m0s 9 | user: testbot 10 | group: testbot 11 | persistent: true 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | go.work.sum 23 | 24 | bin/ 25 | build/ 26 | .codegpt 27 | -------------------------------------------------------------------------------- /utils/system_helpers.go: -------------------------------------------------------------------------------- 1 | package rpiutils 2 | 3 | import ( 4 | "os/exec" 5 | 6 | "go.viam.com/rdk/logging" 7 | ) 8 | 9 | // PerformReboot attempts to reboot the system using multiple fallback methods. 10 | // It tries systemctl first, then sudo shutdown, and finally logs a warning if both fail. 11 | func PerformReboot(logger logging.Logger) { 12 | 13 | if err := exec.Command("systemctl", "reboot").Run(); err != nil { 14 | logger.Debugf("systemctl reboot failed: %v", err) 15 | 16 | // TODO: Do you need sudo here? 17 | if err := exec.Command("sudo", "shutdown", "-r", "now").Run(); err != nil { 18 | logger.Debugf("sudo shutdown failed: %v", err) 19 | 20 | logger.Warnf("Automatic reboot failed. Please manually reboot the system for I2C changes to take effect: sudo reboot") 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // package main is a module with raspberry pi board component. 2 | package main 3 | 4 | import ( 5 | "go.viam.com/rdk/components/board" 6 | "go.viam.com/rdk/components/servo" 7 | "go.viam.com/rdk/module" 8 | "go.viam.com/rdk/resource" 9 | "raspberry-pi/pi5" 10 | "raspberry-pi/rpi" 11 | rpiservo "raspberry-pi/rpi-servo" 12 | ) 13 | 14 | func main() { 15 | module.ModularMain( 16 | resource.APIModel{board.API, pi5.Model}, 17 | resource.APIModel{board.API, rpi.ModelPi}, 18 | resource.APIModel{board.API, rpi.ModelPi4}, 19 | resource.APIModel{board.API, rpi.ModelPi3}, 20 | resource.APIModel{board.API, rpi.ModelPi2}, 21 | resource.APIModel{board.API, rpi.ModelPi1}, 22 | resource.APIModel{board.API, rpi.ModelPi0_2}, 23 | resource.APIModel{board.API, rpi.ModelPi0}, 24 | resource.APIModel{servo.API, rpiservo.Model}) 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Build & publish to registry 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build_and_lint: 9 | runs-on: buildjet-4vcpu-ubuntu-2204-arm 10 | container: ghcr.io/viam-modules/raspberry-pi:arm64 11 | timeout-minutes: 30 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Verify no uncommitted changes from make lint 16 | run: | 17 | git init 18 | git add . 19 | chown -R testbot:testbot . 20 | sudo -u testbot bash -lc 'make lint' 21 | if [ -n "$GEN_DIFF" ]; then 22 | echo '"make lint" resulted in changes not in git' 1>&2 23 | git status 24 | exit 1 25 | fi 26 | 27 | - name: make module 28 | run: | 29 | sudo -u testbot bash -lc 'make module' 30 | 31 | - name: Upload 32 | uses: viamrobotics/upload-module@main 33 | with: 34 | module-path: bin/raspberry-pi-module.tar.gz 35 | platform: linux/arm64 36 | version: ${{ github.ref_name }} 37 | ref: ${{ github.sha }} 38 | key-id: ${{ secrets.VIAM_DEV_API_KEY_ID }} 39 | key-value: ${{ secrets.VIAM_DEV_API_KEY }} 40 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Pigpio client libraries 4 | DEBIAN_FRONTEND=noninteractive apt install -qqy libpigpiod-if2-1 2>&1 5 | 6 | # RPI5 doesn't need pigpiod 7 | WHATPI=$(awk '{print $3}' /proc/device-tree/model) 8 | if [ "$WHATPI" = "5" ]; then 9 | exec ./bin/raspberry-pi-arm64 "$@" "pi5-detected" 10 | fi 11 | 12 | ARCH=$(uname -m) 13 | 14 | # Check if pigpiod is already running. 15 | # NOTE: it may be running as a service or locally started process. No attempt is made to control it. 16 | if pgrep -x "pigpiod" > /dev/null; then 17 | echo "pigpiod is already running, not explicitly starting it." 18 | else 19 | echo "pigpiod is not running, starting..." 20 | 21 | if [ "$ARCH" = "aarch64" ]; then 22 | # 64-bit ARM architecture 23 | ./bin/pigpiod-arm64/pigpiod -l 24 | else 25 | # 32-bit ARM architecture 26 | ./bin/pigpiod-arm/pigpiod -l 27 | fi 28 | 29 | sleep 1 30 | 31 | if pgrep -x "pigpiod" > /dev/null; then 32 | echo "pigpiod started successfully." 33 | else 34 | echo "pigpiod failed to start." >&2 35 | exit 1 36 | fi 37 | fi 38 | 39 | if [ "$ARCH" = "aarch64" ]; then 40 | # 64-bit ARM architecture 41 | exec ./bin/raspberry-pi-arm64 "$@" 42 | else 43 | # 32-bit ARM architecture 44 | exec ./bin/raspberry-pi-arm "$@" 45 | fi 46 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:bullseye 2 | 3 | ARG GO_VERSION=1.25.1 4 | 5 | RUN --mount=type=cache,target=/var/cache/apt apt-get update 6 | RUN --mount=type=cache,target=/var/cache/apt \ 7 | apt-get install -qqy \ 8 | git build-essential make curl gpg wget sudo nano file procps bash cmake 9 | 10 | RUN curl -L https://go.dev/dl/go${GO_VERSION}.linux-$(if [ `dpkg --print-architecture` = arm64 ]; then echo arm64; else echo armv6l; fi).tar.gz | tar -xzv -C /usr/local && \ 11 | update-alternatives --install /usr/bin/go go /usr/local/go/bin/go 10 \ 12 | --slave /usr/bin/gofmt gofmt /usr/local/go/bin/gofmt 13 | 14 | # Raspberry Pi repo 15 | RUN curl http://archive.raspberrypi.org/debian/raspberrypi.gpg.key -o /etc/apt/trusted.gpg.d/raspberrypi.asc && echo "deb http://archive.raspberrypi.com/debian $(grep VERSION_CODENAME /etc/os-release | cut -d= -f2) main" > /etc/apt/sources.list.d/raspi.list 16 | RUN --mount=type=cache,target=/var/cache/apt apt-get update 17 | 18 | RUN --mount=type=cache,target=/var/cache/apt \ 19 | apt-get install -qqy pigpio 20 | 21 | RUN useradd -s /bin/bash -m testbot && echo 'testbot ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers 22 | 23 | # Fix for github and new security patches on git 24 | RUN git config --system --add safe.directory '*' 25 | 26 | # Verify installations 27 | RUN go version 28 | 29 | CMD ["/bin/bash"] 30 | -------------------------------------------------------------------------------- /utils/config.go: -------------------------------------------------------------------------------- 1 | // Package rpiutils contains implementations for digital_interrupts here. 2 | package rpiutils 3 | 4 | import ( 5 | "fmt" 6 | 7 | "go.viam.com/rdk/components/board/mcp3008helper" 8 | "go.viam.com/rdk/resource" 9 | ) 10 | 11 | // RaspiFamily is the model family for the Raspberry Pi module. 12 | var RaspiFamily = resource.NewModelFamily("viam", "raspberry-pi") 13 | 14 | // BoardSettings contains board-level configuration options. 15 | type BoardSettings struct { 16 | TurnI2COn bool `json:"turn_i2c_on,omitempty"` 17 | BTenableuart *bool `json:"bluetooth_enable_uart,omitempty"` 18 | BTdtoverlay *bool `json:"bluetooth_dtoverlay_miniuart,omitempty"` 19 | BTkbaudrate *int `json:"bluetooth_baud_rate,omitempty"` 20 | } 21 | 22 | // A Config describes the configuration of a board and all of its connected parts. 23 | type Config struct { 24 | AnalogReaders []mcp3008helper.MCP3008AnalogConfig `json:"analogs,omitempty"` 25 | Pins []PinConfig `json:"pins,omitempty"` 26 | BoardSettings BoardSettings `json:"board_settings,omitempty"` 27 | } 28 | 29 | // Validate ensures all parts of the config are valid. 30 | func (conf *Config) Validate(path string) ([]string, []string, error) { 31 | for idx, c := range conf.AnalogReaders { 32 | if err := c.Validate(fmt.Sprintf("%s.%s.%d", path, "analogs", idx)); err != nil { 33 | return nil, nil, err 34 | } 35 | } 36 | 37 | for _, c := range conf.Pins { 38 | if err := c.Validate(path); err != nil { 39 | return nil, nil, err 40 | } 41 | } 42 | return nil, nil, nil 43 | } 44 | -------------------------------------------------------------------------------- /pi5/board_test.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | package pi5 4 | 5 | import ( 6 | "context" 7 | "testing" 8 | 9 | "go.viam.com/rdk/components/board/genericlinux" 10 | "go.viam.com/rdk/logging" 11 | "go.viam.com/rdk/resource" 12 | "go.viam.com/test" 13 | rpiutils "raspberry-pi/utils" 14 | ) 15 | 16 | func TestEmptyBoard(t *testing.T) { 17 | b := &pinctrlpi5{ 18 | logger: logging.NewTestLogger(t), 19 | } 20 | 21 | t.Run("test empty sysfs board", func(t *testing.T) { 22 | _, err := b.GPIOPinByName("10") 23 | test.That(t, err, test.ShouldNotBeNil) 24 | }) 25 | } 26 | 27 | func TestNewBoard(t *testing.T) { 28 | logger := logging.NewTestLogger(t) 29 | ctx := context.Background() 30 | 31 | // Create a fake board mapping with two pins for testing. 32 | // BoardMappings are needed as a parameter passed in to NewBoard but are not used for pin control testing yet. 33 | testBoardMappings := make(map[string]genericlinux.GPIOBoardMapping, 0) 34 | conf := &rpiutils.Config{} 35 | config := resource.Config{ 36 | Name: "board1", 37 | ConvertedAttributes: conf, 38 | } 39 | 40 | // Test Creations of Boards 41 | newB, err := newBoard(ctx, config, testBoardMappings, logger, true) 42 | test.That(t, err, test.ShouldBeNil) 43 | test.That(t, newB, test.ShouldNotBeNil) 44 | defer newB.Close(ctx) 45 | 46 | // Cast from board.Board to pinctrlpi5 is required to access board's vars 47 | p5 := newB.(*pinctrlpi5) 48 | test.That(t, p5.boardPinCtrl.Cfg.ChipSize, test.ShouldEqual, 0x30000) 49 | testVal := uint64(0x1f000d0000) 50 | test.That(t, p5.boardPinCtrl.PhysAddr, test.ShouldEqual, testVal) 51 | } 52 | -------------------------------------------------------------------------------- /rpi-servo/config.go: -------------------------------------------------------------------------------- 1 | // Package rpiservo contains servo config to ensure it is valid with a pin and board name. 2 | package rpiservo 3 | 4 | import ( 5 | "github.com/pkg/errors" 6 | "go.viam.com/rdk/resource" 7 | ) 8 | 9 | // ServoConfig is the config for a pi servo. 10 | type ServoConfig struct { 11 | BoardName string `json:"board"` 12 | Pin string `json:"pin"` 13 | 14 | Min int `json:"min,omitempty"` // specifies a user inputted minimum position limitation 15 | Max int `json:"max,omitempty"` // specifies a user inputted maximum position limitation 16 | StartPos *float64 `json:"starting_position_degs,omitempty"` // specifies a starting position. Defaults to 90 17 | HoldPos *bool `json:"hold_position,omitempty"` // defaults True. False holds for 500 ms then disables servo 18 | MaxRotation int `json:"max_rotation_deg,omitempty"` // specifies a hardware position limitation. Defaults to 180 19 | Freq int `json:"frequency_hz,omitempty"` // specifies the pwm frequency to drive the servo. 20 | } 21 | 22 | // Validate ensures all parts of the config are valid. 23 | func (config *ServoConfig) Validate(path string) ([]string, []string, error) { 24 | var deps []string 25 | if config.Pin == "" { 26 | return nil, nil,resource.NewConfigValidationError(path, 27 | errors.New("need pin for pi servo")) 28 | } 29 | if config.BoardName == "" { 30 | return nil, nil, resource.NewConfigValidationError(path, 31 | errors.New("need the name of the board")) 32 | } 33 | deps = append(deps, config.BoardName) 34 | return deps, nil, nil 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | pull_request: 7 | 8 | jobs: 9 | build_and_lint: 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | architecture: [arm, arm64] 14 | runs-on: buildjet-4vcpu-ubuntu-2204-arm 15 | container: 16 | image: ghcr.io/viam-modules/raspberry-pi:${{ matrix.architecture }} 17 | volumes: 18 | # override /__e/node20 on 32-bit because it is 64-bit 19 | - /tmp/node20:/__e${{matrix.architecture != 'arm' && '-armhf' || ''}}/node20 20 | timeout-minutes: 30 21 | 22 | steps: 23 | - name: Unbork NodeJS for 32-bit 24 | if: matrix.architecture == 'arm' 25 | run: | 26 | curl -L https://nodejs.org/download/release/v20.18.0/node-v20.18.0-linux-armv7l.tar.xz | tar -xJ -C /__e/node20 --strip-components=1 --overwrite 27 | file /__e/node20/bin/node 28 | /__e/node20/bin/node --version 29 | echo "Node Installed" 30 | 31 | - uses: actions/checkout@v3 32 | 33 | - name: Verify no uncommitted changes from make lint 34 | run: | 35 | git init 36 | git add . 37 | chown -R testbot:testbot . 38 | sudo -u testbot bash -lc 'make lint' 39 | if [ -n "$GEN_DIFF" ]; then 40 | echo '"make lint" resulted in changes not in git' 1>&2 41 | git status 42 | exit 1 43 | fi 44 | 45 | - name: make build 46 | run: | 47 | sudo -u testbot bash -lc 'make' 48 | 49 | # test_on_pi: 50 | # # The following steps are run on an external runner "rpibull" 51 | # runs-on: pi-4 52 | 53 | # steps: 54 | # - uses: actions/checkout@v3 55 | # - run: make test 56 | -------------------------------------------------------------------------------- /rpi/pi.c: -------------------------------------------------------------------------------- 1 | /* 2 | pi.c: This file is a bridge to setup interrupts for the Raspberry Pi GPIO pins 3 | using the pigpiod library. It uses a callback function pigpioInterruptCallback 4 | for interrupt handling, which is exported within rpi.go. 5 | */ 6 | #include 7 | 8 | extern void pigpioInterruptCallback(int gpio, int level, uint32_t tick); 9 | 10 | // interruptCallback calls through to the go linked interrupt callback. 11 | void interruptCallback(int pi, unsigned gpio, unsigned level, uint32_t tick) { 12 | if (level == 2) { 13 | // watchdog 14 | return; 15 | } 16 | pigpioInterruptCallback(gpio, level, tick); 17 | } 18 | 19 | int setupInterrupt(int pi, int gpio) { 20 | int result = set_mode(pi, gpio, PI_INPUT); 21 | if (result != 0) { 22 | return result; 23 | } 24 | result = set_pull_up_down(pi, gpio, PI_PUD_UP); // should this be configurable? 25 | if (result != 0) { 26 | return result; 27 | } 28 | // successful call returns a callback ID that can be used to cancel the callback 29 | result = callback(pi, gpio, EITHER_EDGE, interruptCallback); 30 | return result; 31 | } 32 | int setPullUp(int pi, int gpio) { 33 | int result = set_pull_up_down(pi, gpio, PI_PUD_UP); 34 | return result; 35 | 36 | } 37 | 38 | int setPullDown(int pi, int gpio) { 39 | int result = set_pull_up_down(pi, gpio, PI_PUD_DOWN); 40 | return result; 41 | } 42 | 43 | int setPullNone(int pi, int gpio) { 44 | int result = set_pull_up_down(pi, gpio, PI_PUD_OFF); 45 | return result; 46 | } 47 | 48 | int teardownInterrupt(unsigned int callback_id) { 49 | int result = callback_cancel(callback_id); 50 | // Do we need to unset the pullup resistors? 51 | return result; 52 | } 53 | -------------------------------------------------------------------------------- /utils/system_helpers_test.go: -------------------------------------------------------------------------------- 1 | package rpiutils 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "testing" 7 | 8 | "go.viam.com/rdk/logging" 9 | "go.viam.com/test" 10 | ) 11 | 12 | func TestPerformReboot(t *testing.T) { 13 | logger := logging.NewTestLogger(t) 14 | 15 | // Skip this test if running in CI or non-root environment 16 | // since we can't actually test system reboot commands 17 | if os.Getenv("CI") != "" || os.Getuid() != 0 { 18 | t.Skip("Skipping reboot test in CI or non-root environment") 19 | } 20 | 21 | t.Run("reboot_commands_exist", func(t *testing.T) { 22 | // Test that the reboot commands exist and are executable 23 | // This doesn't actually run them, just checks they exist 24 | 25 | // Check if systemctl exists 26 | _, err := exec.LookPath("systemctl") 27 | systemctlExists := err == nil 28 | 29 | // Check if sudo exists 30 | _, err = exec.LookPath("sudo") 31 | sudoExists := err == nil 32 | 33 | // Check if shutdown exists 34 | _, err = exec.LookPath("shutdown") 35 | shutdownExists := err == nil 36 | 37 | // At least one reboot method should be available 38 | hasRebootMethod := systemctlExists || (sudoExists && shutdownExists) 39 | test.That(t, hasRebootMethod, test.ShouldBeTrue) 40 | 41 | // Call PerformReboot in a way that doesn't actually reboot 42 | // This will test the command construction and error handling 43 | // without actually rebooting the system 44 | 45 | // We can't easily test the actual reboot without mocking, 46 | // but we can at least ensure the function doesn't panic 47 | defer func() { 48 | if r := recover(); r != nil { 49 | t.Errorf("PerformReboot panicked: %v", r) 50 | } 51 | }() 52 | 53 | // Note: This will likely fail with permission errors in test environment, 54 | // but that's expected and better than actually rebooting 55 | PerformReboot(logger) 56 | }) 57 | } -------------------------------------------------------------------------------- /utils/broadcom.go: -------------------------------------------------------------------------------- 1 | // Package rpiutils contains implementations for switching between Broadcom to physical pin. 2 | package rpiutils 3 | 4 | import "fmt" 5 | 6 | // DefaultPWMFreqHz is the default pwm frequency used for pwms on raspberry pis. 7 | // Original default from libpigpio. 8 | const DefaultPWMFreqHz = uint(800) 9 | 10 | // piHWPinToBroadcom maps the hardware inscribed pin number to 11 | // its Broadcom pin. For the sake of programming, a user typically 12 | // knows the hardware pin since they have the board on hand but does 13 | // not know the corresponding Broadcom pin. 14 | var piHWPinToBroadcom = map[string]uint{ 15 | // 1 -> 3v3 16 | // 2 -> 5v 17 | "3": 2, 18 | "sda": 2, 19 | // 4 -> 5v 20 | "5": 3, 21 | "scl": 3, 22 | // 6 -> GND 23 | "7": 4, 24 | "8": 14, 25 | // 9 -> GND 26 | "10": 15, 27 | "11": 17, 28 | "12": 18, 29 | "clk": 18, 30 | "13": 27, 31 | // 14 -> GND 32 | "15": 22, 33 | "16": 23, 34 | // 17 -> 3v3 35 | "18": 24, 36 | "19": 10, 37 | "mosi": 10, 38 | // 20 -> GND 39 | "21": 9, 40 | "miso": 9, 41 | "22": 25, 42 | "23": 11, 43 | "sclk": 11, 44 | "24": 8, 45 | "ce0": 8, 46 | // 25 -> GND 47 | "26": 7, 48 | "ce1": 7, 49 | "27": 0, 50 | "28": 1, 51 | "29": 5, 52 | // 30 -> GND 53 | "31": 6, 54 | "32": 12, 55 | "33": 13, 56 | // 34 -> GND 57 | "35": 19, 58 | "36": 16, 59 | "37": 26, 60 | "38": 20, 61 | // 39 -> GND 62 | "40": 21, 63 | } 64 | 65 | // TODO: we should agree on one config standard for pin definitions 66 | // instead of doing this. Maybe just use the actual pin number? 67 | // It might be reasonable to force users to look up the associations 68 | // online - GV 69 | 70 | // BroadcomPinFromHardwareLabel returns a Raspberry Pi pin number given 71 | // a hardware label for the pin passed from a config. 72 | func BroadcomPinFromHardwareLabel(hwPin string) (uint, bool) { 73 | // check if we were given a hardware pin & return the broadcom label if so 74 | pin, ok := piHWPinToBroadcom[hwPin] 75 | if ok { 76 | return pin, true 77 | } 78 | // if we weren't given a hardware pin, check if we were given a broadcom label 79 | for _, existingVal := range piHWPinToBroadcom { 80 | if hwPin == fmt.Sprintf("io%d", existingVal) { 81 | return existingVal, true 82 | } 83 | } 84 | return 1000, false 85 | } 86 | -------------------------------------------------------------------------------- /rpi/analog_reader.go: -------------------------------------------------------------------------------- 1 | package rpi 2 | 3 | /* 4 | This file implements analog reader functionality for the Raspberry Pi. 5 | */ 6 | 7 | import ( 8 | "strconv" 9 | 10 | "github.com/pkg/errors" 11 | "go.viam.com/rdk/components/board" 12 | "go.viam.com/rdk/components/board/genericlinux/buses" 13 | "go.viam.com/rdk/components/board/mcp3008helper" 14 | "go.viam.com/rdk/components/board/pinwrappers" 15 | rpiutils "raspberry-pi/utils" 16 | ) 17 | 18 | // Helper functions to configure analog readers and interrupts. 19 | func (pi *piPigpio) reconfigureAnalogReaders(cfg *rpiutils.Config) error { 20 | // No need to reconfigure the old analog readers; just throw them out and make new ones. 21 | pi.analogReaders = map[string]*pinwrappers.AnalogSmoother{} 22 | for _, ac := range cfg.AnalogReaders { 23 | channel, err := strconv.Atoi(ac.Channel) 24 | if err != nil { 25 | return errors.Errorf("bad analog pin (%s)", ac.Channel) 26 | } 27 | 28 | chipSelect := ac.ChipSelect 29 | 30 | // Use genericlinux implementation for SPI bus. 31 | switch chipSelect { 32 | case "24", "ce0", "io8", "0": 33 | // HW pin 24 maps to chip select 0 34 | chipSelect = "0" 35 | case "26", "ce1", "io7", "1": 36 | // HW pin 26 maps to chip select 1 37 | chipSelect = "1" 38 | default: 39 | return errors.Errorf("bad chip select (%s), choose chip select 0 (pin 24) or 1 (pin 26)", chipSelect) 40 | } 41 | 42 | bus := buses.NewSpiBus(ac.SPIBus) 43 | 44 | ar := &mcp3008helper.MCP3008AnalogReader{ 45 | Channel: channel, 46 | Bus: bus, 47 | Chip: chipSelect, 48 | } 49 | 50 | pi.analogReaders[ac.Name] = pinwrappers.SmoothAnalogReader(ar, board.AnalogReaderConfig{ 51 | AverageOverMillis: ac.AverageOverMillis, SamplesPerSecond: ac.SamplesPerSecond, 52 | }, pi.logger) 53 | } 54 | return nil 55 | } 56 | 57 | // AnalogNames returns the names of all known analog pins. 58 | func (pi *piPigpio) AnalogNames() []string { 59 | pi.mu.Lock() 60 | defer pi.mu.Unlock() 61 | names := []string{} 62 | for k := range pi.analogReaders { 63 | names = append(names, k) 64 | } 65 | return names 66 | } 67 | 68 | // AnalogByName returns an analog pin by name. 69 | func (pi *piPigpio) AnalogByName(name string) (board.Analog, error) { 70 | pi.mu.Lock() 71 | defer pi.mu.Unlock() 72 | a, ok := pi.analogReaders[name] 73 | if !ok { 74 | return nil, errors.Errorf("can't find Analog pin (%s)", name) 75 | } 76 | return a, nil 77 | } 78 | -------------------------------------------------------------------------------- /tests/rpiservo_external_test.go: -------------------------------------------------------------------------------- 1 | package rpi_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "go.viam.com/rdk/components/servo" 8 | "go.viam.com/rdk/logging" 9 | "go.viam.com/rdk/resource" 10 | "go.viam.com/test" 11 | rpiservo "raspberry-pi/rpi-servo" 12 | ) 13 | 14 | func TestPiServo(t *testing.T) { 15 | ctx := context.Background() 16 | logger := logging.NewTestLogger(t) 17 | 18 | t.Run("servo initialize with pin error", func(t *testing.T) { 19 | servoReg, ok := resource.LookupRegistration(servo.API, rpiservo.Model) 20 | test.That(t, ok, test.ShouldBeTrue) 21 | test.That(t, servoReg, test.ShouldNotBeNil) 22 | _, err := servoReg.Constructor( 23 | ctx, 24 | nil, 25 | resource.Config{ 26 | Name: "servo", 27 | ConvertedAttributes: &rpiservo.ServoConfig{Pin: ""}, 28 | }, 29 | logger, 30 | ) 31 | test.That(t, err.Error(), test.ShouldContainSubstring, "need pin for pi servo") 32 | }) 33 | 34 | t.Run("check new servo defaults", func(t *testing.T) { 35 | ctx := context.Background() 36 | servoReg, ok := resource.LookupRegistration(servo.API, rpiservo.Model) 37 | test.That(t, ok, test.ShouldBeTrue) 38 | test.That(t, servoReg, test.ShouldNotBeNil) 39 | servoInt, err := servoReg.Constructor( 40 | ctx, 41 | nil, 42 | resource.Config{ 43 | Name: "servo", 44 | ConvertedAttributes: &rpiservo.ServoConfig{Pin: "22"}, 45 | }, 46 | logger, 47 | ) 48 | test.That(t, err, test.ShouldBeNil) 49 | 50 | servo1 := servoInt.(servo.Servo) 51 | pos1, err := servo1.Position(ctx, nil) 52 | test.That(t, err, test.ShouldBeNil) 53 | test.That(t, pos1, test.ShouldEqual, 90) 54 | }) 55 | 56 | t.Run("check set default position", func(t *testing.T) { 57 | ctx := context.Background() 58 | servoReg, ok := resource.LookupRegistration(servo.API, rpiservo.Model) 59 | test.That(t, ok, test.ShouldBeTrue) 60 | test.That(t, servoReg, test.ShouldNotBeNil) 61 | 62 | initPos := 33.0 63 | servoInt, err := servoReg.Constructor( 64 | ctx, 65 | nil, 66 | resource.Config{ 67 | Name: "servo", 68 | ConvertedAttributes: &rpiservo.ServoConfig{Pin: "22", StartPos: &initPos}, 69 | }, 70 | logger, 71 | ) 72 | test.That(t, err, test.ShouldBeNil) 73 | 74 | servo1 := servoInt.(servo.Servo) 75 | pos1, err := servo1.Position(ctx, nil) 76 | test.That(t, err, test.ShouldBeNil) 77 | test.That(t, pos1, test.ShouldEqual, 33) 78 | }) 79 | } 80 | -------------------------------------------------------------------------------- /meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "module_id": "viam:raspberry-pi", 3 | "visibility": "public", 4 | "url": "https://github.com/viam-modules/raspberry-pi", 5 | "description": "Viam Go module for raspberry pi board and servo", 6 | "models": [ 7 | { 8 | "api": "rdk:component:board", 9 | "model": "viam:raspberry-pi:rpi", 10 | "markdown_link": "README.md#configure-your-raspberry-pi-board", 11 | "short_description": "A board component for the Raspberry Pi GPIO pins." 12 | }, 13 | { 14 | "api": "rdk:component:board", 15 | "model": "viam:raspberry-pi:rpi5", 16 | "markdown_link": "README.md#configure-your-raspberry-pi-board", 17 | "short_description": "A board component for the Raspberry Pi 5 GPIO pins." 18 | }, 19 | { 20 | "api": "rdk:component:board", 21 | "model": "viam:raspberry-pi:rpi4", 22 | "markdown_link": "README.md#configure-your-raspberry-pi-board", 23 | "short_description": "A board component for the Raspberry Pi 4 GPIO pins." 24 | }, 25 | { 26 | "api": "rdk:component:board", 27 | "model": "viam:raspberry-pi:rpi3", 28 | "markdown_link": "README.md#configure-your-raspberry-pi-board", 29 | "short_description": "A board component for the Raspberry Pi 3 GPIO pins." 30 | }, 31 | { 32 | "api": "rdk:component:board", 33 | "model": "viam:raspberry-pi:rpi2", 34 | "markdown_link": "README.md#configure-your-raspberry-pi-board", 35 | "short_description": "A board component for the Raspberry Pi 2 GPIO pins." 36 | }, 37 | { 38 | "api": "rdk:component:board", 39 | "model": "viam:raspberry-pi:rpi1", 40 | "markdown_link": "README.md#configure-your-raspberry-pi-board", 41 | "short_description": "A board component for the Raspberry Pi 1 GPIO pins." 42 | }, 43 | { 44 | "api": "rdk:component:board", 45 | "model": "viam:raspberry-pi:rpi0", 46 | "markdown_link": "README.md#configure-your-raspberry-pi-board", 47 | "short_description": "A board component for the Raspberry Pi 0 GPIO pins." 48 | }, 49 | { 50 | "api": "rdk:component:board", 51 | "model": "viam:raspberry-pi:rpi0_2", 52 | "markdown_link": "README.md#configure-your-raspberry-pi-board", 53 | "short_description": "A board component for the Raspberry Pi 0 2 GPIO pins." 54 | }, 55 | { 56 | "api": "rdk:component:servo", 57 | "model": "viam:raspberry-pi:rpi-servo" 58 | , 59 | "markdown_link": "README.md#configure-your-pi-servo", 60 | "short_description": "A servo to run a servo on the Raspberry Pi 0 to 4." 61 | } 62 | ], 63 | "build": { 64 | "build": "make setup module", 65 | "path": "bin/raspberry-pi-module.tar.gz", 66 | "arch" : ["linux/arm64"] 67 | }, 68 | "entrypoint": "run.sh" 69 | } 70 | -------------------------------------------------------------------------------- /etc/.golangci.yaml: -------------------------------------------------------------------------------- 1 | service: 2 | golangci-lint-version: 1.51.x 3 | run: 4 | deadline: 900s 5 | modules-download-mode: readonly 6 | linters: 7 | enable-all: true 8 | disable: 9 | - asasalint 10 | - mnd 11 | - containedctx 12 | - contextcheck 13 | - cyclop 14 | - deadcode 15 | - depguard 16 | - exhaustivestruct 17 | - exhaustruct 18 | - forcetypeassert 19 | - funlen 20 | - gocognit 21 | - godox 22 | - goerr113 23 | - gochecknoglobals 24 | - gochecknoinits 25 | - gocyclo 26 | - gofmt 27 | - goimports 28 | - golint 29 | - gomnd 30 | - ifshort 31 | - importas 32 | - interfacebloat 33 | - interfacer 34 | - ireturn 35 | - maintidx 36 | - maligned 37 | - makezero 38 | - musttag 39 | - nakedret 40 | - nestif 41 | - nlreturn 42 | - nosnakecase 43 | - nonamedreturns 44 | - nosprintfhostport 45 | - paralleltest 46 | - prealloc 47 | - scopelint 48 | - structcheck 49 | - tagliatelle 50 | - testpackage 51 | - thelper # false positives 52 | - varcheck 53 | - varnamelen 54 | - wrapcheck 55 | - wsl 56 | linters-settings: 57 | errcheck: 58 | check-blank: true 59 | gci: 60 | sections: 61 | - standard 62 | - default 63 | gofumpt: 64 | lang-version: "1.23" 65 | extra-rules: true 66 | gosec: 67 | excludes: 68 | - G115 69 | govet: 70 | enable-all: true 71 | disable: 72 | - fieldalignment 73 | - shadow 74 | - composites 75 | revive: 76 | # Unfortunately configuring a single rules disables all other rules, even 77 | # if we set `enable-all: true` 78 | # 79 | # To get around this, we include default rules: 80 | # https://github.com/mgechev/revive/blob/master/defaults.toml 81 | rules: 82 | - name: blank-imports 83 | - name: context-as-argument 84 | disabled: false 85 | arguments: 86 | - allowTypesBefore: "testing.TB,*testing.T,*testing.B,*testing.F" 87 | - name: context-keys-type 88 | - name: dot-imports 89 | - name: empty-block 90 | - name: error-naming 91 | - name: error-return 92 | - name: error-strings 93 | - name: errorf 94 | - name: exported 95 | - name: increment-decrement 96 | - name: indent-error-flow 97 | - name: package-comments 98 | - name: range 99 | - name: receiver-naming 100 | - name: redefines-builtin-id 101 | - name: superfluous-else 102 | - name: time-naming 103 | - name: unexported-return 104 | - name: unreachable-code 105 | - name: var-declaration 106 | - name: var-naming 107 | lll: 108 | line-length: 140 109 | issues: 110 | exclude-rules: 111 | - path: _test\.go$ 112 | linters: 113 | - dupword 114 | - errcheck 115 | - exhaustive 116 | - goconst 117 | - gosec 118 | exclude-use-default: false 119 | max-per-linter: 0 120 | max-same-issues: 0 -------------------------------------------------------------------------------- /pi5/data.go: -------------------------------------------------------------------------------- 1 | // Package pi5 implements a raspberry pi5 board using pinctrl 2 | package pi5 3 | 4 | import "go.viam.com/rdk/components/board/genericlinux" 5 | 6 | // Thanks to "Dan Makes Things" at https://www.makerforge.tech/posts/viam-custom-board-pi5/ for 7 | // collaborating on setting this up! 8 | var boardInfoMappings = map[string]genericlinux.BoardInformation{ 9 | "pi5": { 10 | PinDefinitions: []genericlinux.PinDefinition{ 11 | {Name: "3", DeviceName: "gpiochip4", LineNumber: 2, PwmChipSysfsDir: "", PwmID: -1}, 12 | {Name: "5", DeviceName: "gpiochip4", LineNumber: 3, PwmChipSysfsDir: "", PwmID: -1}, 13 | {Name: "7", DeviceName: "gpiochip4", LineNumber: 4, PwmChipSysfsDir: "", PwmID: -1}, 14 | {Name: "8", DeviceName: "gpiochip4", LineNumber: 14, PwmChipSysfsDir: "", PwmID: -1}, 15 | {Name: "10", DeviceName: "gpiochip4", LineNumber: 15, PwmChipSysfsDir: "", PwmID: -1}, 16 | {Name: "11", DeviceName: "gpiochip4", LineNumber: 17, PwmChipSysfsDir: "", PwmID: -1}, 17 | {Name: "12", DeviceName: "gpiochip4", LineNumber: 18, PwmChipSysfsDir: "1f00098000.pwm", PwmID: 2}, 18 | {Name: "13", DeviceName: "gpiochip4", LineNumber: 27, PwmChipSysfsDir: "", PwmID: -1}, 19 | {Name: "15", DeviceName: "gpiochip4", LineNumber: 22, PwmChipSysfsDir: "", PwmID: -1}, 20 | {Name: "16", DeviceName: "gpiochip4", LineNumber: 23, PwmChipSysfsDir: "", PwmID: -1}, 21 | {Name: "18", DeviceName: "gpiochip4", LineNumber: 24, PwmChipSysfsDir: "", PwmID: -1}, 22 | {Name: "19", DeviceName: "gpiochip4", LineNumber: 10, PwmChipSysfsDir: "", PwmID: -1}, 23 | {Name: "21", DeviceName: "gpiochip4", LineNumber: 9, PwmChipSysfsDir: "", PwmID: -1}, 24 | {Name: "22", DeviceName: "gpiochip4", LineNumber: 25, PwmChipSysfsDir: "", PwmID: -1}, 25 | {Name: "23", DeviceName: "gpiochip4", LineNumber: 11, PwmChipSysfsDir: "", PwmID: -1}, 26 | {Name: "24", DeviceName: "gpiochip4", LineNumber: 8, PwmChipSysfsDir: "", PwmID: -1}, 27 | {Name: "26", DeviceName: "gpiochip4", LineNumber: 7, PwmChipSysfsDir: "", PwmID: -1}, 28 | // Per https://www.raspberrypi.com/documentation/computers/images/GPIO-duplicate.png 29 | // Physical pins 27 and 28 (shown in white in that diagram) should not be used for 30 | // normal GPIO stuff. 31 | {Name: "29", DeviceName: "gpiochip4", LineNumber: 5, PwmChipSysfsDir: "", PwmID: -1}, 32 | {Name: "31", DeviceName: "gpiochip4", LineNumber: 6, PwmChipSysfsDir: "", PwmID: -1}, 33 | // We'd expect pins 32 and 33 to have hardware PWM support, too, but we haven't gotten 34 | // that to work yet. 35 | {Name: "32", DeviceName: "gpiochip4", LineNumber: 12, PwmChipSysfsDir: "", PwmID: -1}, 36 | {Name: "33", DeviceName: "gpiochip4", LineNumber: 13, PwmChipSysfsDir: "", PwmID: -1}, 37 | {Name: "35", DeviceName: "gpiochip4", LineNumber: 19, PwmChipSysfsDir: "1f00098000.pwm", PwmID: 3}, 38 | {Name: "36", DeviceName: "gpiochip4", LineNumber: 16, PwmChipSysfsDir: "", PwmID: -1}, 39 | {Name: "37", DeviceName: "gpiochip4", LineNumber: 26, PwmChipSysfsDir: "", PwmID: -1}, 40 | {Name: "38", DeviceName: "gpiochip4", LineNumber: 20, PwmChipSysfsDir: "", PwmID: -1}, 41 | {Name: "40", DeviceName: "gpiochip4", LineNumber: 21, PwmChipSysfsDir: "", PwmID: -1}, 42 | }, 43 | Compats: []string{"raspberrypi,5-model-b", "brcm,bcm2712"}, 44 | }, 45 | } 46 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BIN_OUTPUT_PATH = bin 2 | BUILD_OUTPUT_PATH = build 3 | TOOL_BIN = bin/gotools/$(shell uname -s)-$(shell uname -m) 4 | 5 | DPKG_ARCH ?= $(shell dpkg --print-architecture) 6 | ifeq ($(DPKG_ARCH),armhf) 7 | DOCKER_ARCH ?= arm 8 | else ifeq ($(DPKG_ARCH),arm64) 9 | DOCKER_ARCH ?= arm64 10 | else 11 | DOCKER_ARCH ?= unknown 12 | endif 13 | 14 | OUTPUT_PATH = $(BIN_OUTPUT_PATH)/raspberry-pi-$(DOCKER_ARCH) 15 | 16 | IMAGE_NAME = ghcr.io/viam-modules/raspberry-pi 17 | 18 | .PHONY: module 19 | module: build-$(DOCKER_ARCH) $(BIN_OUTPUT_PATH)/pigpiod-$(DOCKER_ARCH)/pigpiod 20 | rm -f $(BIN_OUTPUT_PATH)/raspberry-pi-module.tar.gz 21 | cp $(BIN_OUTPUT_PATH)/raspberry-pi-$(DOCKER_ARCH) $(BIN_OUTPUT_PATH)/raspberry-pi 22 | tar czf $(BIN_OUTPUT_PATH)/raspberry-pi-module.tar.gz $(BIN_OUTPUT_PATH)/raspberry-pi-$(DOCKER_ARCH) $(BIN_OUTPUT_PATH)/pigpiod-$(DOCKER_ARCH) run.sh meta.json 23 | 24 | .PHONY: build-$(DOCKER_ARCH) 25 | build-$(DOCKER_ARCH): 26 | go build -o $(BIN_OUTPUT_PATH)/raspberry-pi-$(DOCKER_ARCH) main.go 27 | 28 | $(BIN_OUTPUT_PATH)/pigpiod-$(DOCKER_ARCH)/pigpiod: 29 | mkdir -p $(BIN_OUTPUT_PATH)/pigpiod-$(DOCKER_ARCH) 30 | mkdir -p $(BUILD_OUTPUT_PATH) 31 | cd $(BUILD_OUTPUT_PATH) && \ 32 | wget https://github.com/joan2937/pigpio/archive/refs/tags/v79.tar.gz && \ 33 | tar zxf v79.tar.gz && \ 34 | cd pigpio-79 && \ 35 | cmake -DBUILD_SHARED_LIBS=off . && \ 36 | make pigpiod 37 | cp $(BUILD_OUTPUT_PATH)/pigpio-79/pigpiod $(BIN_OUTPUT_PATH)/pigpiod-$(DOCKER_ARCH) 38 | 39 | .PHONY: update-rdk 40 | update-rdk: 41 | go get go.viam.com/rdk@latest 42 | go mod tidy 43 | 44 | .PHONY: test 45 | test: 46 | go test -c -o $(BIN_OUTPUT_PATH)/raspberry-pi-tests-$(DOCKER_ARCH)/ ./... 47 | for test in $$(ls $(BIN_OUTPUT_PATH)/raspberry-pi-tests-$(DOCKER_ARCH)/*.test) ; do \ 48 | sudo $$test -test.v || exit $?; \ 49 | done 50 | 51 | .PHONY: tool-install 52 | tool-install: $(TOOL_BIN)/golangci-lint 53 | 54 | $(TOOL_BIN)/golangci-lint: 55 | GOBIN=`pwd`/$(TOOL_BIN) go install github.com/golangci/golangci-lint/cmd/golangci-lint 56 | 57 | .PHONY: lint 58 | lint: $(TOOL_BIN)/golangci-lint 59 | go mod tidy 60 | $(TOOL_BIN)/golangci-lint run -v --fix --config=./etc/.golangci.yaml --timeout 5m 61 | 62 | .PHONY: docker-all 63 | docker-all: docker-build-64 docker-build-32 64 | 65 | .PHONY: docker-build 66 | docker-build: 67 | cd docker && docker buildx build --load --no-cache --platform linux/$(DOCKER_ARCH) -t $(IMAGE_NAME):$(DOCKER_ARCH) . 68 | 69 | .PHONY: docker-build-64 70 | docker-build-64: 71 | DOCKER_ARCH=arm64 make docker-build 72 | 73 | .PHONY: docker-build-32 74 | docker-build-32: 75 | DOCKER_ARCH=arm make docker-build 76 | 77 | .PHONY: docker-upload-all 78 | docker-upload-all: docker-push-arm32 docker-push-arm64 docker-manifest 79 | 80 | .PHONY: docker-push-arm32 81 | docker-push-arm32: 82 | docker push $(IMAGE_NAME):arm 83 | 84 | .PHONY: docker-push-arm64 85 | docker-push-arm64: 86 | docker push $(IMAGE_NAME):arm64 87 | 88 | .PHONY: docker-manifest 89 | docker-manifest: 90 | docker manifest create --amend $(IMAGE_NAME):latest $(IMAGE_NAME):arm $(IMAGE_NAME):arm64 91 | docker manifest push $(IMAGE_NAME):latest 92 | 93 | .PHONY: setup 94 | setup: 95 | sudo apt install -yqq libpigpiod-if2-1 96 | 97 | clean: 98 | rm -rf $(BIN_OUTPUT_PATH) $(BUILD_OUTPUT_PATH) 99 | -------------------------------------------------------------------------------- /utils/digital_interrupts_test.go: -------------------------------------------------------------------------------- 1 | package rpiutils 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | "testing" 7 | "time" 8 | 9 | "go.viam.com/rdk/components/board" 10 | "go.viam.com/test" 11 | ) 12 | 13 | func nowNanosecondsTest() uint64 { 14 | return uint64(time.Now().UnixNano()) 15 | } 16 | 17 | func TestBasicDigitalInterrupt1(t *testing.T) { 18 | config := PinConfig{ 19 | Name: "i1", 20 | Type: "interrupt", 21 | } 22 | 23 | i, err := CreateDigitalInterrupt(config) 24 | test.That(t, err, test.ShouldBeNil) 25 | 26 | basicInterrupt := i.(*BasicDigitalInterrupt) 27 | 28 | intVal, err := i.Value(context.Background(), nil) 29 | test.That(t, err, test.ShouldBeNil) 30 | test.That(t, intVal, test.ShouldEqual, int64(0)) 31 | test.That(t, Tick(context.Background(), basicInterrupt, true, nowNanosecondsTest()), test.ShouldBeNil) 32 | intVal, err = i.Value(context.Background(), nil) 33 | test.That(t, err, test.ShouldBeNil) 34 | test.That(t, intVal, test.ShouldEqual, int64(1)) 35 | test.That(t, Tick(context.Background(), basicInterrupt, false, nowNanosecondsTest()), test.ShouldBeNil) 36 | intVal, err = i.Value(context.Background(), nil) 37 | test.That(t, err, test.ShouldBeNil) 38 | test.That(t, intVal, test.ShouldEqual, int64(1)) 39 | 40 | c := make(chan board.Tick) 41 | AddCallback(basicInterrupt, c) 42 | 43 | timeNanoSec := nowNanosecondsTest() 44 | go func() { Tick(context.Background(), basicInterrupt, true, timeNanoSec) }() 45 | time.Sleep(1 * time.Microsecond) 46 | v := <-c 47 | test.That(t, v.High, test.ShouldBeTrue) 48 | test.That(t, v.TimestampNanosec, test.ShouldEqual, timeNanoSec) 49 | 50 | timeNanoSec = nowNanosecondsTest() 51 | go func() { Tick(context.Background(), basicInterrupt, true, timeNanoSec) }() 52 | v = <-c 53 | test.That(t, v.High, test.ShouldBeTrue) 54 | test.That(t, v.TimestampNanosec, test.ShouldEqual, timeNanoSec) 55 | 56 | RemoveCallback(basicInterrupt, c) 57 | 58 | c = make(chan board.Tick, 2) 59 | AddCallback(basicInterrupt, c) 60 | go func() { 61 | Tick(context.Background(), basicInterrupt, true, uint64(1)) 62 | Tick(context.Background(), basicInterrupt, true, uint64(4)) 63 | }() 64 | v = <-c 65 | v1 := <-c 66 | test.That(t, v.High, test.ShouldBeTrue) 67 | test.That(t, v1.High, test.ShouldBeTrue) 68 | test.That(t, v1.TimestampNanosec-v.TimestampNanosec, test.ShouldEqual, uint32(3)) 69 | } 70 | 71 | func TestRemoveCallbackDigitalInterrupt(t *testing.T) { 72 | config := PinConfig{ 73 | Name: "d1", 74 | Type: "interrupt", 75 | } 76 | i, err := CreateDigitalInterrupt(config) 77 | test.That(t, err, test.ShouldBeNil) 78 | basicInterrupt := i.(*BasicDigitalInterrupt) 79 | intVal, err := i.Value(context.Background(), nil) 80 | test.That(t, err, test.ShouldBeNil) 81 | test.That(t, intVal, test.ShouldEqual, int64(0)) 82 | test.That(t, Tick(context.Background(), basicInterrupt, true, nowNanosecondsTest()), test.ShouldBeNil) 83 | intVal, err = i.Value(context.Background(), nil) 84 | test.That(t, err, test.ShouldBeNil) 85 | test.That(t, intVal, test.ShouldEqual, int64(1)) 86 | 87 | c1 := make(chan board.Tick) 88 | test.That(t, c1, test.ShouldNotBeNil) 89 | AddCallback(basicInterrupt, c1) 90 | var wg sync.WaitGroup 91 | wg.Add(1) 92 | ret := false 93 | 94 | go func() { 95 | defer wg.Done() 96 | select { 97 | case <-context.Background().Done(): 98 | return 99 | default: 100 | } 101 | select { 102 | case <-context.Background().Done(): 103 | return 104 | case tick := <-c1: 105 | ret = tick.High 106 | } 107 | }() 108 | test.That(t, Tick(context.Background(), basicInterrupt, true, nowNanosecondsTest()), test.ShouldBeNil) 109 | intVal, err = i.Value(context.Background(), nil) 110 | test.That(t, err, test.ShouldBeNil) 111 | test.That(t, intVal, test.ShouldEqual, int64(2)) 112 | wg.Wait() 113 | c2 := make(chan board.Tick) 114 | test.That(t, c2, test.ShouldNotBeNil) 115 | AddCallback(basicInterrupt, c2) 116 | test.That(t, ret, test.ShouldBeTrue) 117 | 118 | RemoveCallback(basicInterrupt, c1) 119 | RemoveCallback(basicInterrupt, c1) 120 | 121 | ret2 := false 122 | result := make(chan bool, 1) 123 | go func() { 124 | defer wg.Done() 125 | select { 126 | case <-context.Background().Done(): 127 | return 128 | default: 129 | } 130 | select { 131 | case <-context.Background().Done(): 132 | return 133 | case tick := <-c2: 134 | ret2 = tick.High 135 | } 136 | }() 137 | wg.Add(1) 138 | go func() { 139 | err := Tick(context.Background(), basicInterrupt, true, nowNanosecondsTest()) 140 | if err != nil { 141 | result <- true 142 | } 143 | result <- true 144 | }() 145 | select { 146 | case <-time.After(1 * time.Second): 147 | ret = false 148 | case ret = <-result: 149 | } 150 | wg.Wait() 151 | test.That(t, ret, test.ShouldBeTrue) 152 | test.That(t, ret2, test.ShouldBeTrue) 153 | intVal, err = i.Value(context.Background(), nil) 154 | test.That(t, err, test.ShouldBeNil) 155 | test.That(t, intVal, test.ShouldEqual, int64(3)) 156 | } 157 | -------------------------------------------------------------------------------- /rpi-servo/rpiservo_helpers.go: -------------------------------------------------------------------------------- 1 | package rpiservo 2 | 3 | // #include 4 | // #include 5 | // #include "../rpi/pi.h" 6 | import "C" 7 | 8 | import ( 9 | "fmt" 10 | 11 | "github.com/pkg/errors" 12 | "go.viam.com/rdk/resource" 13 | rpiutils "raspberry-pi/utils" 14 | ) 15 | 16 | // Validate and set piPigpioServo fields based on the configuration. 17 | func (s *piPigpioServo) validateAndSetConfiguration(conf *ServoConfig) error { 18 | if conf.Min >= 0 { 19 | s.min = uint32(conf.Min) 20 | } 21 | 22 | s.max = 180 23 | if conf.Max > 0 { 24 | s.max = uint32(conf.Max) 25 | } 26 | s.maxRotation = uint32(conf.MaxRotation) 27 | if s.maxRotation == 0 { 28 | s.maxRotation = uint32(servoDefaultMaxRotation) 29 | } 30 | if s.maxRotation < s.min { 31 | return errors.New("maxRotation is less than minimum") 32 | } 33 | if s.maxRotation < s.max { 34 | return errors.New("maxRotation is less than maximum") 35 | } 36 | 37 | // if user doesn't provide a frequency, we keep the default value of 50 Hz 38 | if conf.Freq > 0 { 39 | s.pwmFreqHz = C.uint(conf.Freq) 40 | } 41 | 42 | s.pinname = conf.Pin 43 | 44 | return nil 45 | } 46 | 47 | // setInitialPosition sets the initial position of the servo based on the provided configuration. 48 | func setInitialPosition(piServo *piPigpioServo, newConf *ServoConfig) error { 49 | position := 1500 50 | if newConf.StartPos != nil { 51 | position = angleToPulseWidth(int(*newConf.StartPos), int(piServo.maxRotation)) 52 | } 53 | err := piServo.setServoPulseWidth(position) 54 | if err != nil { 55 | return err 56 | } 57 | return nil 58 | } 59 | 60 | // handleHoldPosition configures the hold position setting for the servo. 61 | func handleHoldPosition(piServo *piPigpioServo, newConf *ServoConfig) error { 62 | if newConf.HoldPos == nil || *newConf.HoldPos { 63 | // Hold the servo position 64 | piServo.holdPos = true 65 | } else { 66 | // Release the servo position and disable the servo 67 | piServo.pwInUse = C.get_PWM_dutycycle(piServo.piID, piServo.pin) 68 | piServo.holdPos = false 69 | err := piServo.setServoPulseWidth(0) 70 | if err != nil { 71 | return errors.New("erroring setting pulse width to 0") 72 | } 73 | } 74 | return nil 75 | } 76 | 77 | // sets the servo's pulse width 78 | func (s *piPigpioServo) setServoPulseWidth(pulseWidth int) error { 79 | // Check if pulse width is within the valid range 80 | if pulseWidth < 0 || pulseWidth > 2500 { 81 | return errors.New("invalid pulse width: out of range [0, 2500]") 82 | } 83 | 84 | errCode := C.set_PWM_frequency(s.piID, s.pin, s.pwmFreqHz) 85 | if errCode < 0 { 86 | return fmt.Errorf("servo set pwm frequency on pin %s failed: %w", s.pinname, s.pigpioErrors(int(errCode))) 87 | } 88 | errCode = C.set_PWM_range(s.piID, s.pin, 1e6/s.pwmFreqHz) 89 | if errCode < 0 { 90 | return fmt.Errorf("servo set pwm range on pin %s failed: %w", s.pinname, s.pigpioErrors(int(errCode))) 91 | } 92 | errCode = C.set_PWM_dutycycle(s.piID, s.pin, C.uint(pulseWidth)) 93 | if errCode < 0 { 94 | return fmt.Errorf("servo set pwm duty cycle on pin %s failed: %w", s.pinname, s.pigpioErrors(int(errCode))) 95 | } 96 | return nil 97 | } 98 | 99 | // parseConfig parses the provided configuration into a ServoConfig. 100 | func parseConfig(conf resource.Config) (*ServoConfig, error) { 101 | newConf, err := resource.NativeConfig[*ServoConfig](conf) 102 | if err != nil { 103 | return nil, err 104 | } 105 | return newConf, nil 106 | } 107 | 108 | // validateConfig validates the provided ServoConfig. 109 | func validateConfig(newConf *ServoConfig) error { 110 | if newConf.Pin == "" { 111 | return errors.New("need pin for pi servo") 112 | } 113 | return nil 114 | } 115 | 116 | // getBroadcomPin retrieves the Broadcom pin number from the hardware label. 117 | func getBroadcomPin(pin string) (uint, error) { 118 | bcom, have := rpiutils.BroadcomPinFromHardwareLabel(pin) 119 | if !have { 120 | return 0, errors.Errorf("no hw mapping for %s", pin) 121 | } 122 | return bcom, nil 123 | } 124 | 125 | // pigpioErrors returns piGPIO specific errors to user 126 | func (s *piPigpioServo) pigpioErrors(res int) error { 127 | switch { 128 | case res == C.PI_NOT_SERVO_GPIO: 129 | return errors.Errorf("servo on pin %s is not set up to send and receive pulsewidths", s.pinname) 130 | case res == C.PI_BAD_PULSEWIDTH: 131 | return errors.Errorf("servo on pin %s trying to reach out of range position", s.pinname) 132 | case res == 0: 133 | return nil 134 | case res < 0 && res != C.PI_BAD_PULSEWIDTH && res != C.PI_NOT_SERVO_GPIO: 135 | errMsg := fmt.Sprintf("gpioServo on pin %s failed", s.pinname) 136 | return rpiutils.ConvertErrorCodeToMessage(res, errMsg) 137 | default: 138 | return nil 139 | } 140 | } 141 | 142 | // angleToPulseWidth changes the input angle in degrees 143 | // into the corresponding pulsewidth value in microsecond 144 | func angleToPulseWidth(angle, maxRotation int) int { 145 | pulseWidth := 500 + (2000 * angle / maxRotation) 146 | return pulseWidth 147 | } 148 | 149 | // pulseWidthToAngle changes the pulsewidth value in microsecond 150 | // to the corresponding angle in degrees 151 | func pulseWidthToAngle(pulseWidth, maxRotation int) int { 152 | angle := maxRotation * (pulseWidth + 1 - 500) / 2000 153 | return angle 154 | } 155 | -------------------------------------------------------------------------------- /utils/digital_interrupts.go: -------------------------------------------------------------------------------- 1 | // Package rpiutils contains implementations for digital_interrupts here. 2 | package rpiutils 3 | 4 | import ( 5 | "context" 6 | "fmt" 7 | "sync" 8 | "sync/atomic" 9 | 10 | "github.com/pkg/errors" 11 | "go.viam.com/rdk/components/board" 12 | "go.viam.com/rdk/resource" 13 | ) 14 | 15 | // PinConfig describes the configuration of a pin for the board. 16 | type PinConfig struct { 17 | Name string `json:"name"` 18 | Pin string `json:"pin"` 19 | Type PinType `json:"type,omitempty"` // e.g. gpio, interrupt 20 | DebounceMS int `json:"debounce_ms,omitempty"` // only used with interrupts 21 | PullState Pull `json:"pull,omitempty"` 22 | } 23 | 24 | // PinType defines the pin types we support. 25 | type PinType string 26 | 27 | const ( 28 | // PinGPIO represents GPIO pins. 29 | PinGPIO PinType = "gpio" 30 | // PinInterrupt represents interrupt pins. 31 | PinInterrupt PinType = "interrupt" 32 | ) 33 | 34 | // Pull defines the pins pull state(pull up vs pull down). 35 | type Pull string 36 | 37 | const ( 38 | // PullUp is for pull ups. 39 | PullUp Pull = "up" 40 | // PullDown is for pull downs. 41 | PullDown Pull = "down" 42 | // PullNone is for no pulls. 43 | PullNone Pull = "none" 44 | // PullDefault is for if no pull was set. 45 | PullDefault Pull = "" 46 | ) 47 | 48 | // Validate validates that the pull is a valid message. 49 | func (pull Pull) Validate() error { 50 | switch pull { 51 | case PullDefault, PullUp, PullDown, PullNone: 52 | default: 53 | return fmt.Errorf("invalid pull configuration %v, supported pull config attributes are up, down, and none", pull) 54 | } 55 | return nil 56 | } 57 | 58 | // Validate ensures all parts of the config are valid. 59 | func (config *PinConfig) Validate(path string) error { 60 | if config.Pin == "" { 61 | return resource.NewConfigValidationFieldRequiredError(path, "pin") 62 | } 63 | if err := config.PullState.Validate(); err != nil { 64 | return err 65 | } 66 | return nil 67 | } 68 | 69 | // ServoRollingAverageWindow is how many entries to average over for 70 | // servo ticks. 71 | const ServoRollingAverageWindow = 10 72 | 73 | // A ReconfigurableDigitalInterrupt is a simple reconfigurable digital interrupt that expects 74 | // reconfiguration within the same type. 75 | type ReconfigurableDigitalInterrupt interface { 76 | board.DigitalInterrupt 77 | Reconfigure(cfg PinConfig) error 78 | } 79 | 80 | // CreateDigitalInterrupt is a factory method for creating a specific DigitalInterrupt based 81 | // on the given config. If no type is specified, an error is returned. 82 | func CreateDigitalInterrupt(cfg PinConfig) (ReconfigurableDigitalInterrupt, error) { 83 | i := &BasicDigitalInterrupt{} 84 | //nolint:exhaustive 85 | switch cfg.Type { 86 | case PinInterrupt: 87 | default: 88 | return nil, fmt.Errorf("expected pin %v to be configured as %v, got %v instead", cfg.Name, PinInterrupt, cfg.Type) 89 | } 90 | 91 | if err := i.Reconfigure(cfg); err != nil { 92 | return nil, err 93 | } 94 | return i, nil 95 | } 96 | 97 | // A BasicDigitalInterrupt records how many ticks/interrupts happen and can 98 | // report when they happen to interested callbacks. 99 | type BasicDigitalInterrupt struct { 100 | count int64 101 | 102 | callbacks []chan board.Tick 103 | 104 | mu sync.RWMutex 105 | cfg PinConfig 106 | } 107 | 108 | // Value returns the amount of ticks that have occurred. 109 | func (i *BasicDigitalInterrupt) Value(ctx context.Context, extra map[string]interface{}) (int64, error) { 110 | i.mu.RLock() 111 | defer i.mu.RUnlock() 112 | count := atomic.LoadInt64(&i.count) 113 | return count, nil 114 | } 115 | 116 | // Tick records an interrupt and notifies any interested callbacks. See comment on 117 | // the DigitalInterrupt interface for caveats. 118 | func Tick(ctx context.Context, i *BasicDigitalInterrupt, high bool, nanoseconds uint64) error { 119 | if high { 120 | atomic.AddInt64(&i.count, 1) 121 | } 122 | 123 | i.mu.RLock() 124 | defer i.mu.RUnlock() 125 | for _, c := range i.callbacks { 126 | select { 127 | case <-ctx.Done(): 128 | return errors.New("context cancelled") 129 | case c <- board.Tick{Name: i.cfg.Name, High: high, TimestampNanosec: nanoseconds}: 130 | } 131 | } 132 | return nil 133 | } 134 | 135 | // AddCallback adds a listener for interrupts. 136 | func AddCallback(i *BasicDigitalInterrupt, c chan board.Tick) { 137 | i.mu.Lock() 138 | defer i.mu.Unlock() 139 | i.callbacks = append(i.callbacks, c) 140 | } 141 | 142 | // RemoveCallback removes a listener for interrupts. 143 | func RemoveCallback(i *BasicDigitalInterrupt, c chan board.Tick) { 144 | i.mu.Lock() 145 | defer i.mu.Unlock() 146 | for id := range i.callbacks { 147 | if i.callbacks[id] == c { 148 | // To remove this item, we replace it with the last item in the list, then truncate the 149 | // list by 1. 150 | i.callbacks[id] = i.callbacks[len(i.callbacks)-1] 151 | i.callbacks = i.callbacks[:len(i.callbacks)-1] 152 | break 153 | } 154 | } 155 | } 156 | 157 | // Name returns the name of the interrupt. 158 | func (i *BasicDigitalInterrupt) Name() string { 159 | i.mu.Lock() 160 | defer i.mu.Unlock() 161 | return i.cfg.Name 162 | } 163 | 164 | // Reconfigure reconfigures this digital interrupt. 165 | func (i *BasicDigitalInterrupt) Reconfigure(conf PinConfig) error { 166 | i.mu.Lock() 167 | defer i.mu.Unlock() 168 | i.cfg = conf 169 | return nil 170 | } 171 | -------------------------------------------------------------------------------- /tests/rpi_external_test.go: -------------------------------------------------------------------------------- 1 | /* rpi_test runs same tests as rpi_test, but using exported board functions only */ 2 | package rpi_test 3 | 4 | import ( 5 | "context" 6 | "os" 7 | "testing" 8 | "time" 9 | 10 | "go.viam.com/rdk/components/board" 11 | "go.viam.com/rdk/components/servo" 12 | "go.viam.com/rdk/logging" 13 | "go.viam.com/rdk/resource" 14 | "go.viam.com/test" 15 | "raspberry-pi/rpi" 16 | rpiservo "raspberry-pi/rpi-servo" 17 | rpiutils "raspberry-pi/utils" 18 | ) 19 | 20 | func TestPiPigpio(t *testing.T) { 21 | piReg, ok := resource.LookupRegistration(board.API, rpi.ModelPi4) 22 | test.That(t, ok, test.ShouldBeTrue) 23 | test.That(t, piReg, test.ShouldNotBeNil) 24 | 25 | ctx := context.Background() 26 | logger := logging.NewTestLogger(t) 27 | 28 | cfg := rpiutils.Config{ 29 | Pins: []rpiutils.PinConfig{ 30 | {Name: "i1", Pin: "11", Type: "interrupt"}, // bcom 17 31 | }, 32 | } 33 | 34 | piInt, err := piReg.Constructor( 35 | ctx, 36 | nil, 37 | resource.Config{ 38 | Name: "rpi", 39 | ConvertedAttributes: &cfg, 40 | }, 41 | logger, 42 | ) 43 | 44 | if os.Getuid() != 0 || err != nil && err.Error() == "not running on a pi" { 45 | t.Skip("not running as root on a pi") 46 | return 47 | } 48 | 49 | test.That(t, err, test.ShouldBeNil) 50 | p := piInt.(board.Board) 51 | 52 | defer func() { 53 | err := p.Close(ctx) 54 | test.That(t, err, test.ShouldBeNil) 55 | }() 56 | 57 | t.Run("external gpio and pwm", func(t *testing.T) { 58 | pin, err := p.GPIOPinByName("29") 59 | test.That(t, err, test.ShouldBeNil) 60 | 61 | // try to set high 62 | err = pin.Set(ctx, true, nil) 63 | test.That(t, err, test.ShouldBeNil) 64 | 65 | v, err := pin.Get(ctx, nil) 66 | test.That(t, err, test.ShouldBeNil) 67 | test.That(t, v, test.ShouldEqual, true) 68 | 69 | // try to set low 70 | err = pin.Set(ctx, false, nil) 71 | test.That(t, err, test.ShouldBeNil) 72 | 73 | v, err = pin.Get(ctx, nil) 74 | test.That(t, err, test.ShouldBeNil) 75 | test.That(t, v, test.ShouldEqual, false) 76 | 77 | // pwm 50% 78 | err = pin.SetPWM(ctx, 0.5, nil) 79 | test.That(t, err, test.ShouldBeNil) 80 | 81 | vF, err := pin.PWM(ctx, nil) 82 | test.That(t, err, test.ShouldBeNil) 83 | test.That(t, vF, test.ShouldAlmostEqual, 0.5, 0.01) 84 | 85 | // 4000 hz 86 | err = pin.SetPWMFreq(ctx, 4000, nil) 87 | test.That(t, err, test.ShouldBeNil) 88 | 89 | vI, err := pin.PWMFreq(ctx, nil) 90 | test.That(t, err, test.ShouldBeNil) 91 | test.That(t, vI, test.ShouldEqual, 4000) 92 | 93 | // 90% 94 | err = pin.SetPWM(ctx, 0.9, nil) 95 | test.That(t, err, test.ShouldBeNil) 96 | 97 | vF, err = pin.PWM(ctx, nil) 98 | test.That(t, err, test.ShouldBeNil) 99 | test.That(t, vF, test.ShouldAlmostEqual, 0.9, 0.01) 100 | 101 | // 8000hz 102 | err = pin.SetPWMFreq(ctx, 8000, nil) 103 | test.That(t, err, test.ShouldBeNil) 104 | 105 | vI, err = pin.PWMFreq(ctx, nil) 106 | test.That(t, err, test.ShouldBeNil) 107 | test.That(t, vI, test.ShouldEqual, 8000) 108 | }) 109 | 110 | // interrupt is configured on pi board creation 111 | t.Run("external preconfigured basic interrupt test", func(t *testing.T) { 112 | // Test interrupt i1 on pin 11 (bcom 17) 113 | i1, err := p.DigitalInterruptByName("i1") 114 | test.That(t, err, test.ShouldBeNil) 115 | 116 | // set pin state to LOW for interrupt test 117 | pin, err := p.GPIOPinByName("11") 118 | test.That(t, err, test.ShouldBeNil) 119 | err = pin.Set(ctx, false, nil) 120 | test.That(t, err, test.ShouldBeNil) 121 | 122 | time.Sleep(5 * time.Millisecond) 123 | 124 | before, err := i1.Value(context.Background(), nil) 125 | test.That(t, err, test.ShouldBeNil) 126 | 127 | // set pin state to HIGH to trigger interrupt 128 | err = pin.Set(ctx, true, nil) 129 | test.That(t, err, test.ShouldBeNil) 130 | 131 | time.Sleep(5 * time.Millisecond) 132 | 133 | after, err := i1.Value(context.Background(), nil) 134 | test.That(t, err, test.ShouldBeNil) 135 | test.That(t, after-before, test.ShouldEqual, int64(1)) 136 | }) 137 | 138 | // digital interrupt creates by name (on valid pin) 139 | t.Run("external create new basic interrupt test", func(t *testing.T) { 140 | // Set and create interrupt on pin 13 141 | i2, err := p.DigitalInterruptByName("13") 142 | test.That(t, err, test.ShouldBeNil) 143 | 144 | // Set pin 13 (bcom 27) to LOW 145 | pin, err := p.GPIOPinByName("13") 146 | test.That(t, err, test.ShouldBeNil) 147 | err = pin.Set(ctx, false, nil) 148 | test.That(t, err, test.ShouldBeNil) 149 | 150 | time.Sleep(5 * time.Millisecond) 151 | 152 | // interrupt not created, bad pin name 153 | _, err = p.DigitalInterruptByName("some") 154 | test.That(t, err, test.ShouldNotBeNil) 155 | 156 | before, err := i2.Value(context.Background(), nil) 157 | test.That(t, err, test.ShouldBeNil) 158 | 159 | // Set pin 13 (bcom 27) to HIGH 160 | err = pin.Set(ctx, true, nil) 161 | test.That(t, err, test.ShouldBeNil) 162 | 163 | time.Sleep(5 * time.Millisecond) 164 | 165 | after, err := i2.Value(context.Background(), nil) 166 | test.That(t, err, test.ShouldBeNil) 167 | test.That(t, after-before, test.ShouldEqual, int64(1)) 168 | 169 | _, err = p.DigitalInterruptByName("11") 170 | test.That(t, err, test.ShouldBeNil) 171 | }) 172 | 173 | // test servo movement and digital interrupt 174 | // this function is within rpi in order to access piPigpio 175 | t.Run("external servo in/out", func(t *testing.T) { 176 | servoReg, ok := resource.LookupRegistration(servo.API, rpiservo.Model) 177 | test.That(t, ok, test.ShouldBeTrue) 178 | test.That(t, servoReg, test.ShouldNotBeNil) 179 | servoInt, err := servoReg.Constructor( 180 | ctx, 181 | nil, 182 | resource.Config{ 183 | Name: "servo", 184 | ConvertedAttributes: &rpiservo.ServoConfig{Pin: "22"}, 185 | }, 186 | logger, 187 | ) 188 | test.That(t, err, test.ShouldBeNil) 189 | servo1 := servoInt.(servo.Servo) 190 | 191 | // Move to 90 deg and check position 192 | err = servo1.Move(ctx, 90, nil) 193 | test.That(t, err, test.ShouldBeNil) 194 | 195 | v, err := servo1.Position(ctx, nil) 196 | test.That(t, err, test.ShouldBeNil) 197 | test.That(t, int(v), test.ShouldEqual, 90) 198 | 199 | // should move to max position even though 190 is out of range 200 | err = servo1.Move(ctx, 190, nil) 201 | test.That(t, err, test.ShouldBeNil) 202 | 203 | v, err = servo1.Position(ctx, nil) 204 | test.That(t, err, test.ShouldBeNil) 205 | test.That(t, int(v), test.ShouldEqual, 180) 206 | 207 | time.Sleep(300 * time.Millisecond) 208 | 209 | // Next position (120 deg) 210 | err = servo1.Move(ctx, 120, nil) 211 | test.That(t, err, test.ShouldBeNil) 212 | 213 | v, err = servo1.Position(ctx, nil) 214 | test.That(t, err, test.ShouldBeNil) 215 | test.That(t, int(v), test.ShouldEqual, 120) 216 | }) 217 | } 218 | -------------------------------------------------------------------------------- /rpi-servo/rpiservo.go: -------------------------------------------------------------------------------- 1 | // Package rpiservo implements pi servo 2 | package rpiservo 3 | 4 | /* 5 | This driver contains various functionalities of a servo motor used in 6 | conjunction with a Raspberry Pi. The servo connects via a GPIO pin and 7 | uses the pi module's pigpio daemon library to control the servo motor. 8 | The servo pin will override the default pin configuration of the pi 9 | module, including PWM frequency and width. 10 | 11 | Servo hardware model: DigiKey - SER0006 DFRobot 12 | https://www.digikey.com/en/products/detail/dfrobot/SER0006/7597224?WT.mc_id=frommaker.io 13 | 14 | Servo datasheet: 15 | http://www.ee.ic.ac.uk/pcheung/teaching/DE1_EE/stores/sg90_datasheet.pdf 16 | */ 17 | 18 | // #include 19 | // #include 20 | // #include "../rpi/pi.h" 21 | // #cgo LDFLAGS: -lpigpiod_if2 22 | import "C" 23 | 24 | import ( 25 | "context" 26 | "time" 27 | 28 | "go.viam.com/rdk/components/servo" 29 | "go.viam.com/rdk/logging" 30 | "go.viam.com/rdk/operation" 31 | "go.viam.com/rdk/resource" 32 | "go.viam.com/utils" 33 | ) 34 | 35 | // Model represents a pi servo model. 36 | var Model = resource.NewModel("viam", "raspberry-pi", "rpi-servo") 37 | 38 | // Default configuration collected from data sheet 39 | var ( 40 | holdTime = 250000000 // 250ms in nanoseconds 41 | servoDefaultMaxRotation = 180 42 | ) 43 | 44 | // init registers a pi servo based on pigpio. 45 | func init() { 46 | resource.RegisterComponent( 47 | servo.API, 48 | Model, 49 | resource.Registration[servo.Servo, *ServoConfig]{ 50 | Constructor: newPiServo, 51 | }, 52 | ) 53 | } 54 | 55 | func newPiServo( 56 | ctx context.Context, 57 | _ resource.Dependencies, 58 | conf resource.Config, 59 | logger logging.Logger, 60 | ) (servo.Servo, error) { 61 | newConf, err := parseConfig(conf) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | if err := validateConfig(newConf); err != nil { 67 | return nil, err 68 | } 69 | 70 | bcom, err := getBroadcomPin(newConf.Pin) 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | piServo, err := initializeServo(conf, logger, bcom, newConf) 76 | if err != nil { 77 | return nil, err 78 | } 79 | 80 | if err := setInitialPosition(piServo, newConf); err != nil { 81 | return nil, err 82 | } 83 | 84 | if err := handleHoldPosition(piServo, newConf); err != nil { 85 | return nil, err 86 | } 87 | 88 | return piServo, nil 89 | } 90 | 91 | // initializeServo creates and initializes the piPigpioServo with the provided configuration and logger. 92 | func initializeServo(conf resource.Config, logger logging.Logger, bcom uint, newConf *ServoConfig) (*piPigpioServo, error) { 93 | piServo := &piPigpioServo{ 94 | Named: conf.ResourceName().AsNamed(), 95 | logger: logger, 96 | pin: C.uint(bcom), 97 | pinname: newConf.Pin, 98 | opMgr: operation.NewSingleOperationManager(), 99 | pwmFreqHz: 50, // default frequency for most pi hobby servos 100 | } 101 | 102 | piServo.logger.Infof("setting default pwm frequency of 50 Hz") 103 | 104 | if err := piServo.validateAndSetConfiguration(newConf); err != nil { 105 | return nil, err 106 | } 107 | 108 | // Start separate connection from board to pigpio daemon 109 | // Needs to be called before using other pigpio functions 110 | piID := C.pigpio_start(nil, nil) 111 | // Set communication ID for servo 112 | piServo.piID = piID 113 | 114 | return piServo, nil 115 | } 116 | 117 | // piPigpioServo implements a servo.Servo using pigpio. 118 | type piPigpioServo struct { 119 | resource.Named 120 | resource.AlwaysRebuild 121 | logger logging.Logger 122 | pin C.uint 123 | pinname string 124 | pwInUse C.int // pulsewidth in use 125 | min, max uint32 126 | opMgr *operation.SingleOperationManager 127 | pulseWidth int // pulsewidth value, 500-2500us is 0-180 degrees, 0 is off 128 | holdPos bool 129 | maxRotation uint32 130 | piID C.int 131 | pwmFreqHz C.uint 132 | } 133 | 134 | // Move moves the servo to the given angle (0-180 degrees) 135 | // This will block until done or a new operation cancels this one 136 | func (s *piPigpioServo) Move(ctx context.Context, angle uint32, extra map[string]interface{}) error { 137 | ctx, done := s.opMgr.New(ctx) 138 | defer done() 139 | 140 | if s.min > 0 && angle < s.min { 141 | angle = s.min 142 | s.logger.Warnf("move angle %d is less than minimum %d, setting default to minimum angle", angle, s.min) 143 | } 144 | if s.max > 0 && angle > s.max { 145 | angle = s.max 146 | s.logger.Warnf("move angle %d is greater than maximum %d, setting default to maximum angle", angle, s.max) 147 | } 148 | pulseWidth := angleToPulseWidth(int(angle), int(s.maxRotation)) 149 | err := s.setServoPulseWidth(pulseWidth) 150 | if err != nil { 151 | return err 152 | } 153 | 154 | s.pulseWidth = pulseWidth 155 | 156 | utils.SelectContextOrWait(ctx, time.Duration(pulseWidth)*time.Microsecond) // duration of pulsewidth send on pin and servo moves 157 | 158 | if !s.holdPos { // the following logic disables a servo once it has reached a position or after a certain amount of time has been reached 159 | time.Sleep(time.Duration(holdTime)) // time before a stop is sent 160 | err := s.setServoPulseWidth(pulseWidth) 161 | if err != nil { 162 | return err 163 | } 164 | } 165 | return nil 166 | } 167 | 168 | // Position returns the current set angle (degrees) of the servo. 169 | func (s *piPigpioServo) Position(ctx context.Context, extra map[string]interface{}) (uint32, error) { 170 | pwInUse := C.get_PWM_dutycycle(s.piID, s.pin) 171 | err := s.pigpioErrors(int(pwInUse)) 172 | if int(pwInUse) != 0 { 173 | s.pwInUse = pwInUse 174 | } 175 | if err != nil { 176 | return 0, err 177 | } 178 | return uint32(pulseWidthToAngle(int(s.pwInUse), int(s.maxRotation))), nil 179 | } 180 | 181 | // Stop stops the servo. It is assumed the servo stops immediately. 182 | func (s *piPigpioServo) Stop(ctx context.Context, extra map[string]interface{}) error { 183 | _, done := s.opMgr.New(ctx) 184 | defer done() 185 | err := s.setServoPulseWidth(0) 186 | if err != nil { 187 | return err 188 | } 189 | return nil 190 | } 191 | 192 | // IsMoving returns whether the servo is actively moving (or attempting to move) under its own power. 193 | func (s *piPigpioServo) IsMoving(ctx context.Context) (bool, error) { 194 | err := s.pigpioErrors(int(s.pwInUse)) 195 | if err != nil { 196 | return false, err 197 | } 198 | if int(s.pwInUse) == 0 { 199 | return false, nil 200 | } 201 | return s.opMgr.OpRunning(), nil 202 | } 203 | 204 | // Close gracefully stops any ongoing operations and disconnects from the pigpio daemon. 205 | func (s *piPigpioServo) Close(ctx context.Context) error { 206 | s.logger.Debug("Stopping pigpio connection") 207 | C.pigpio_stop(s.piID) 208 | 209 | s.logger.Info("Successfully closed pigpio connection") 210 | return nil 211 | } 212 | -------------------------------------------------------------------------------- /rpi/interrupts.go: -------------------------------------------------------------------------------- 1 | package rpi 2 | 3 | /* 4 | This file implements digital interrupt functionality for the Raspberry Pi. 5 | */ 6 | 7 | // #include 8 | // #include 9 | // #include "pi.h" 10 | // #cgo LDFLAGS: -lpigpiod_if2 11 | import "C" 12 | 13 | import ( 14 | "fmt" 15 | "math" 16 | 17 | rpiutils "raspberry-pi/utils" 18 | 19 | "github.com/pkg/errors" 20 | "go.viam.com/rdk/components/board" 21 | ) 22 | 23 | type rpiInterrupt struct { 24 | interrupt rpiutils.ReconfigurableDigitalInterrupt 25 | callbackID C.uint // callback ID to close pi callback connection 26 | lastTicks uint64 27 | debounceMicroSeconds uint64 28 | } 29 | 30 | // findInterruptByName finds an interrupt by its name, such as: "interrupt-1" 31 | func findInterruptByName( 32 | name string, 33 | interrupts map[uint]*rpiInterrupt, 34 | ) (rpiutils.ReconfigurableDigitalInterrupt, bool) { 35 | for _, rpiInterrupt := range interrupts { 36 | if rpiInterrupt.interrupt.Name() == name { 37 | return rpiInterrupt.interrupt, true 38 | } 39 | } 40 | return nil, false 41 | } 42 | 43 | // reconfigureInterrupts reconfigures the digital interrupts based on the new configuration provided. 44 | // It reuses existing interrupts when possible and creates new ones if necessary. 45 | func (pi *piPigpio) reconfigureInterrupts(cfg *rpiutils.Config) error { 46 | // look at previous interrupt config, and see if we removed any 47 | for _, oldConfig := range pi.pinConfigs { 48 | if oldConfig.Type != rpiutils.PinInterrupt { 49 | continue 50 | } 51 | sameInterrupt := false 52 | for _, newConfig := range cfg.Pins { 53 | if newConfig.Type != rpiutils.PinInterrupt { 54 | continue 55 | } 56 | // check if we still have this interrupt 57 | if oldConfig.Name == newConfig.Name && oldConfig.Pin == newConfig.Pin { 58 | sameInterrupt = true 59 | break 60 | } 61 | } 62 | // if we still have the interrupt, don't modify it 63 | if sameInterrupt { 64 | continue 65 | } 66 | // we no longer want this interrupt, so we will remove it 67 | bcom, ok := rpiutils.BroadcomPinFromHardwareLabel(oldConfig.Pin) 68 | if !ok { 69 | return errors.Errorf("cannot find GPIO for unknown pin: %s", oldConfig.Name) 70 | } 71 | interrupt, ok := pi.interrupts[bcom] 72 | if ok { 73 | if result := C.teardownInterrupt(interrupt.callbackID); result != 0 { 74 | return rpiutils.ConvertErrorCodeToMessage(int(result), "error") 75 | } 76 | delete(pi.interrupts, bcom) 77 | } 78 | } 79 | 80 | // Set new interrupts based on config 81 | for _, newConfig := range cfg.Pins { 82 | if newConfig.Type != rpiutils.PinInterrupt { 83 | continue 84 | } 85 | // check if pin is valid 86 | bcom, ok := rpiutils.BroadcomPinFromHardwareLabel(newConfig.Pin) 87 | if !ok { 88 | return errors.Errorf("no hw mapping for %s", newConfig.Pin) 89 | } 90 | 91 | // check if we are already managing pin 92 | interrupt, ok := pi.interrupts[bcom] 93 | if ok { 94 | pi.logger.Infof("interrupt %v is already a tracked interrupt", interrupt.interrupt.Name()) 95 | continue 96 | } 97 | 98 | // create new interrupt 99 | _, err := pi.createNewInterrupt(newConfig, bcom) 100 | if err != nil { 101 | return err 102 | } 103 | } 104 | 105 | return nil 106 | } 107 | 108 | // createNewInterrupt creates a new digital interrupt and sets it up with the specified configuration. 109 | func (pi *piPigpio) createNewInterrupt(newConfig rpiutils.PinConfig, bcom uint) (rpiutils.ReconfigurableDigitalInterrupt, error) { 110 | d, err := rpiutils.CreateDigitalInterrupt(newConfig) 111 | if err != nil { 112 | return nil, err 113 | } 114 | callbackID := C.setupInterrupt(pi.piID, C.int(bcom)) 115 | if callbackID < 0 { 116 | err := rpiutils.ConvertErrorCodeToMessage(int(callbackID), "error") 117 | return nil, errors.Errorf("Unable to set up interrupt on pin %s: %s", newConfig.Name, err) 118 | } 119 | 120 | pi.interrupts[bcom] = &rpiInterrupt{ 121 | interrupt: d, 122 | callbackID: C.uint(callbackID), 123 | debounceMicroSeconds: uint64(newConfig.DebounceMS) * 1000, 124 | } 125 | 126 | return d, nil 127 | } 128 | 129 | // DigitalInterruptNames returns the names of all known digital interrupts. 130 | func (pi *piPigpio) DigitalInterruptNames() []string { 131 | pi.mu.Lock() 132 | defer pi.mu.Unlock() 133 | names := []string{} 134 | for _, rpiInterrupt := range pi.interrupts { 135 | names = append(names, rpiInterrupt.interrupt.Name()) 136 | } 137 | return names 138 | } 139 | 140 | // DigitalInterruptByName returns a digital interrupt by name. 141 | // NOTE: During board setup, if a digital interrupt has not been created 142 | // for a pin, then this function will attempt to create one with the pin 143 | // number as the name. 144 | func (pi *piPigpio) DigitalInterruptByName(name string) (board.DigitalInterrupt, error) { 145 | pi.mu.Lock() 146 | defer pi.mu.Unlock() 147 | d, ok := findInterruptByName(name, pi.interrupts) 148 | if !ok { 149 | if bcom, have := rpiutils.BroadcomPinFromHardwareLabel(name); have { 150 | if d, ok := pi.interrupts[bcom]; ok { 151 | return d.interrupt, nil 152 | } 153 | return pi.createNewInterrupt( 154 | rpiutils.PinConfig{ 155 | Name: name, 156 | Pin: name, 157 | Type: rpiutils.PinInterrupt, 158 | }, bcom) 159 | } 160 | return d, fmt.Errorf("interrupt %s does not exist", name) 161 | } 162 | return d, nil 163 | } 164 | 165 | var ( 166 | lastTick = uint32(0) 167 | // the interrupt callback returns the time since boot in microseconds, but will wrap every ~72 minutes 168 | // we use the tickRollovers global variable to track each time this has occurred, and update the ticks for every active interrupt 169 | // we assume that uint64 will be large enough for us to not worry about the ticks overflowing further 170 | tickRollovers = 0 171 | ) 172 | 173 | //export pigpioInterruptCallback 174 | func pigpioInterruptCallback(gpio, level int, rawTick uint32) { 175 | if rawTick < lastTick { 176 | tickRollovers++ 177 | } 178 | lastTick = rawTick 179 | 180 | // tick is the time since the hardware was started in microseconds. 181 | tick := (uint64(tickRollovers) * uint64(math.MaxUint32)) + uint64(rawTick) 182 | 183 | // global lock to prevent multiple pins from interacting with the board 184 | boardInstanceMu.RLock() 185 | defer boardInstanceMu.RUnlock() 186 | 187 | // boardInstance has to be initialized before callback can be called 188 | if boardInstance == nil { 189 | return 190 | } 191 | interrupt := boardInstance.interrupts[uint(gpio)] 192 | if interrupt == nil { 193 | boardInstance.logger.Infof("no DigitalInterrupt configured for gpio %d", gpio) 194 | return 195 | } 196 | if interrupt.debounceMicroSeconds != 0 && tick-interrupt.lastTicks < interrupt.debounceMicroSeconds { 197 | // we have not passed the debounce time, ignore this interrupt 198 | return 199 | } 200 | high := true 201 | if level == 0 { 202 | high = false 203 | } 204 | switch di := interrupt.interrupt.(type) { 205 | case *rpiutils.BasicDigitalInterrupt: 206 | err := rpiutils.Tick(boardInstance.cancelCtx, di, high, tick*1000) 207 | if err != nil { 208 | boardInstance.logger.Error(err) 209 | } 210 | default: 211 | boardInstance.logger.Error("unknown digital interrupt type") 212 | } 213 | // store the current ticks for debouncing 214 | interrupt.lastTicks = tick 215 | } 216 | -------------------------------------------------------------------------------- /utils/file_helpers.go: -------------------------------------------------------------------------------- 1 | package rpiutils 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "regexp" 8 | "strings" 9 | 10 | "go.viam.com/rdk/logging" 11 | ) 12 | 13 | // UpdateConfigFile atomically updates a configuration file parameter using regexp. 14 | // - Replaces existing uncommented param lines with the desired value 15 | // - Leaves commented lines intact 16 | // - Appends only if the (uncommented) line exists 17 | // - Preserves file permissions (uses os.Stat + os.WriteFile with original mode) 18 | // - Atomic via temp file + rename 19 | func UpdateConfigFile(filePath, paramPrefix, desiredValue string, logger logging.Logger) (bool, error) { 20 | filePath = filepath.Clean(filePath) 21 | fileInfo, err := os.Stat(filePath) 22 | if err != nil { 23 | return false, fmt.Errorf("failed to stat config file %s: %w", filePath, err) 24 | } 25 | 26 | content, err := os.ReadFile(filePath) 27 | if err != nil { 28 | return false, fmt.Errorf("failed to read config file %s: %w", filePath, err) 29 | } 30 | 31 | lines := strings.Split(string(content), "\n") 32 | configChanged := false 33 | hasActiveTarget := false 34 | targetLine := paramPrefix + desiredValue 35 | 36 | // Matches uncommented or commented param lines (skip commented lines below) 37 | re := regexp.MustCompile(fmt.Sprintf(`^\s*#?\s*%s.*$`, regexp.QuoteMeta(targetLine))) 38 | 39 | for i, line := range lines { 40 | if !re.MatchString(line) { 41 | continue 42 | } 43 | 44 | trimmed := strings.TrimSpace(line) 45 | 46 | // Do not modify commented lines 47 | if strings.HasPrefix(trimmed, "#") { 48 | continue 49 | } 50 | 51 | // Existing active param line 52 | if trimmed == targetLine { 53 | hasActiveTarget = true 54 | continue 55 | } 56 | 57 | // Replace an active param line to the target value 58 | lines[i] = targetLine 59 | configChanged = true 60 | hasActiveTarget = true // prevent duplicate append 61 | } 62 | 63 | // Append only if no active target line was found 64 | if !hasActiveTarget { 65 | lines = append(lines, targetLine) 66 | configChanged = true 67 | } 68 | 69 | if !configChanged { 70 | return false, nil 71 | } 72 | 73 | newContent := strings.Join(lines, "\n") 74 | tempFile := filePath + ".tmp" 75 | 76 | if err := os.WriteFile(tempFile, []byte(newContent), fileInfo.Mode()); err != nil { 77 | return false, fmt.Errorf("failed to write temp config file %s: %w", tempFile, err) 78 | } 79 | if err := os.Rename(tempFile, filePath); err != nil { 80 | _ = os.Remove(tempFile) 81 | return false, fmt.Errorf("failed to replace config file %s: %w", filePath, err) 82 | } 83 | 84 | logger.Debugf("Updated %s in %s", paramPrefix, filePath) 85 | return true, nil 86 | } 87 | 88 | // UpdateModuleFile atomically enables or disables a kernel module in /etc/modules. 89 | // It handles commenting/uncommenting existing entries and preserves file permissions. 90 | func UpdateModuleFile(filePath, moduleName string, enable bool, logger logging.Logger) (bool, error) { 91 | filePath = filepath.Clean(filePath) 92 | fileInfo, err := os.Stat(filePath) 93 | if err != nil { 94 | return false, fmt.Errorf("failed to stat modules file %s: %w", filePath, err) 95 | } 96 | 97 | content, err := os.ReadFile(filePath) 98 | if err != nil { 99 | return false, fmt.Errorf("failed to read modules file %s: %w", filePath, err) 100 | } 101 | 102 | lines := strings.Split(string(content), "\n") 103 | moduleFound := false 104 | configChanged := false 105 | 106 | for i, line := range lines { 107 | trimmedLine := strings.TrimSpace(line) 108 | if trimmedLine == moduleName { 109 | moduleFound = true 110 | if !enable { 111 | lines[i] = "#" + line 112 | configChanged = true 113 | } 114 | } else if trimmedLine == "#"+moduleName { 115 | if enable { 116 | lines[i] = moduleName 117 | configChanged = true 118 | moduleFound = true 119 | } else { 120 | moduleFound = true 121 | } 122 | } 123 | } 124 | 125 | if enable && !moduleFound { 126 | lines = append(lines, moduleName) 127 | configChanged = true 128 | } 129 | 130 | if configChanged { 131 | newContent := strings.Join(lines, "\n") 132 | 133 | tempFile := filePath + ".tmp" 134 | if err := os.WriteFile(tempFile, []byte(newContent), fileInfo.Mode()); err != nil { 135 | return false, fmt.Errorf("failed to write temp modules file %s: %w", tempFile, err) 136 | } 137 | 138 | if err := os.Rename(tempFile, filePath); err != nil { 139 | if removeErr := os.Remove(tempFile); removeErr != nil { 140 | logger.Warnf("Failed to clean up temp file %s: %v", tempFile, removeErr) 141 | } 142 | return false, fmt.Errorf("failed to replace modules file %s: %w", filePath, err) 143 | } 144 | 145 | action := "Added" 146 | if !enable { 147 | action = "Disabled" 148 | } 149 | logger.Infof("%s %s in %s", action, moduleName, filePath) 150 | } 151 | 152 | return configChanged, nil 153 | } 154 | 155 | // GetBootConfigPath returns the correct path for boot config file. 156 | // Handles both /boot/config.txt (older) and /boot/firmware/config.txt (newer). 157 | func GetBootConfigPath() string { 158 | if _, err := os.Stat("/boot/firmware/config.txt"); err == nil { 159 | return "/boot/firmware/config.txt" 160 | } 161 | return "/boot/config.txt" 162 | } 163 | 164 | // RemoveLineMatching removes every uncommented line that matches the given regular expression. 165 | // Returns true if any line was removed. Preserves file permissions and writes atomically. 166 | func RemoveLineMatching(filePath string, lineRegex *regexp.Regexp, logger logging.Logger) (bool, error) { 167 | filePath = filepath.Clean(filePath) 168 | fileInfo, err := os.Stat(filePath) 169 | if err != nil { 170 | return false, fmt.Errorf("failed to stat config file %s: %w", filePath, err) 171 | } 172 | 173 | content, err := os.ReadFile(filePath) 174 | if err != nil { 175 | return false, fmt.Errorf("failed to read config file %s: %w", filePath, err) 176 | } 177 | 178 | origLines := strings.Split(string(content), "\n") 179 | filtered := make([]string, 0, len(origLines)) 180 | removed := false 181 | 182 | for _, line := range origLines { 183 | trimmed := strings.TrimSpace(line) 184 | if strings.HasPrefix(trimmed, "#") { 185 | filtered = append(filtered, line) 186 | continue // skip comments entirely 187 | } 188 | if lineRegex.MatchString(line) { 189 | removed = true 190 | continue 191 | } 192 | filtered = append(filtered, line) 193 | } 194 | 195 | if !removed { 196 | return false, nil 197 | } 198 | 199 | newContent := strings.Join(filtered, "\n") 200 | tempFile := filePath + ".tmp" 201 | if err := os.WriteFile(tempFile, []byte(newContent), fileInfo.Mode()); err != nil { 202 | return false, fmt.Errorf("failed to write temp config file %s: %w", tempFile, err) 203 | } 204 | 205 | if err := os.Rename(tempFile, filePath); err != nil { 206 | _ = os.Remove(tempFile) 207 | return false, fmt.Errorf("failed to replace config file %s: %w", filePath, err) 208 | } 209 | 210 | logger.Debugf("Removed uncommented lines matching %q in %s", lineRegex.String(), filePath) 211 | return true, nil 212 | } 213 | 214 | // RemoveConfigParam removes any *uncommented* line defining the given param (paramPrefix=.*). 215 | func RemoveConfigParam(filePath, paramPrefix string, logger logging.Logger) (bool, error) { 216 | re := regexp.MustCompile(fmt.Sprintf(`^\s*%s.*$`, regexp.QuoteMeta(paramPrefix))) 217 | return RemoveLineMatching(filePath, re, logger) 218 | } 219 | -------------------------------------------------------------------------------- /rpi/gpio.go: -------------------------------------------------------------------------------- 1 | package rpi 2 | 3 | /* 4 | gpio.go: Implements GPIO functionality on Raspberry Pi. 5 | */ 6 | 7 | // #include 8 | // #include 9 | // #include "pi.h" 10 | // #cgo LDFLAGS: -lpigpiod_if2 11 | import "C" 12 | 13 | import ( 14 | "context" 15 | "fmt" 16 | 17 | rpiutils "raspberry-pi/utils" 18 | 19 | "github.com/pkg/errors" 20 | "go.viam.com/rdk/components/board" 21 | rdkutils "go.viam.com/rdk/utils" 22 | ) 23 | 24 | // GPIOConfig tracks what each pin is currently configured as 25 | type GPIOConfig int 26 | 27 | const ( 28 | GPIODefault GPIOConfig = iota // GPIODefault is the default pin state, before we have modified the pin 29 | GPIOInput // GPIOInput is when a pin is configured as a digital input 30 | GPIOOutput // GPIOOutput is when the pin is configured as a digital output 31 | GPIOPWM // GPIOPWM is when the pin is configured as pwm 32 | GPIOInterrupt // GPIOInterrupt is the pin is configured as an interrupt 33 | ) 34 | 35 | type rpiGPIO struct { 36 | name string 37 | pin uint 38 | configuration GPIOConfig 39 | pwmEnabled bool 40 | } 41 | 42 | // GPIOPinByName returns a GPIOPin by name. 43 | func (pi *piPigpio) GPIOPinByName(pin string) (board.GPIOPin, error) { 44 | pi.mu.Lock() 45 | defer pi.mu.Unlock() 46 | 47 | bcom, have := rpiutils.BroadcomPinFromHardwareLabel(pin) 48 | 49 | // check if we have already configured the pin 50 | for _, configuredPin := range pi.gpioPins { 51 | if configuredPin.name == pin { 52 | return gpioPin{pi, int(configuredPin.pin)}, nil 53 | } 54 | // check if the pin was configured with a different name 55 | if have && configuredPin.pin == bcom { 56 | pi.logger.Debugf("pin %v has already been configured with name %v", pin, configuredPin.name) 57 | return gpioPin{pi, int(configuredPin.pin)}, nil 58 | } 59 | } 60 | if !have { 61 | return nil, errors.Errorf("no hw pin for (%s)", pin) 62 | } 63 | 64 | // the pin was not found, so add a new pin to the map 65 | pi.gpioPins[int(bcom)] = &rpiGPIO{pin: bcom, name: pin} 66 | 67 | return gpioPin{pi, int(bcom)}, nil 68 | } 69 | 70 | type gpioPin struct { 71 | pi *piPigpio 72 | bcom int 73 | } 74 | 75 | func (gp gpioPin) Set(ctx context.Context, high bool, extra map[string]interface{}) error { 76 | return gp.pi.SetGPIOBcom(gp.bcom, high) 77 | } 78 | 79 | func (gp gpioPin) Get(ctx context.Context, extra map[string]interface{}) (bool, error) { 80 | return gp.pi.GetGPIOBcom(gp.bcom) 81 | } 82 | 83 | func (gp gpioPin) PWM(ctx context.Context, extra map[string]interface{}) (float64, error) { 84 | return gp.pi.pwmBcom(gp.bcom) 85 | } 86 | 87 | func (gp gpioPin) SetPWM(ctx context.Context, dutyCyclePct float64, extra map[string]interface{}) error { 88 | return gp.pi.SetPWMBcom(gp.bcom, dutyCyclePct) 89 | } 90 | 91 | func (gp gpioPin) PWMFreq(ctx context.Context, extra map[string]interface{}) (uint, error) { 92 | return gp.pi.pwmFreqBcom(gp.bcom) 93 | } 94 | 95 | func (gp gpioPin) SetPWMFreq(ctx context.Context, freqHz uint, extra map[string]interface{}) error { 96 | return gp.pi.SetPWMFreqBcom(gp.bcom, freqHz) 97 | } 98 | 99 | func (pi *piPigpio) reconfigureGPIOs(cfg *rpiutils.Config) error { 100 | // Set new pins based on config 101 | pi.gpioPins = map[int]*rpiGPIO{} 102 | for _, newConfig := range cfg.Pins { 103 | if newConfig.Type != rpiutils.PinGPIO { 104 | continue 105 | } 106 | bcom, have := rpiutils.BroadcomPinFromHardwareLabel(newConfig.Pin) 107 | if !have { 108 | return errors.Errorf("no hw pin for (%s)", newConfig.Pin) 109 | } 110 | pin := &rpiGPIO{name: newConfig.Name, pin: bcom} 111 | pi.gpioPins[int(bcom)] = pin 112 | } 113 | return nil 114 | } 115 | 116 | // GetGPIOBcom gets the level of the given broadcom pin 117 | func (pi *piPigpio) GetGPIOBcom(bcom int) (bool, error) { 118 | pi.mu.Lock() 119 | defer pi.mu.Unlock() 120 | 121 | // verify we are currently managing this pin via GPIOPinByName or reconfigure 122 | pin, ok := pi.gpioPins[bcom] 123 | if !ok { 124 | return false, fmt.Errorf("error getting GPIO pin, pin %v not found", bcom) 125 | } 126 | // configure the pin to be an input if it is not already an input 127 | if pin.configuration != GPIOInput { 128 | res := C.set_mode(pi.piID, C.uint(pin.pin), C.PI_INPUT) 129 | if res != 0 { 130 | return false, rpiutils.ConvertErrorCodeToMessage(int(res), "failed to set mode") 131 | } 132 | } 133 | pin.configuration = GPIOInput 134 | 135 | // gpioRead retrns an int 1 or 0, we convert to a bool 136 | return C.gpio_read(pi.piID, C.uint(bcom)) != 0, nil 137 | } 138 | 139 | // SetGPIOBcom sets the given broadcom pin to high or low. 140 | func (pi *piPigpio) SetGPIOBcom(bcom int, high bool) error { 141 | pi.mu.Lock() 142 | defer pi.mu.Unlock() 143 | 144 | // verify we are currently managing this pin via GPIOPinByName or reconfigure 145 | pin, ok := pi.gpioPins[bcom] 146 | if !ok { 147 | return fmt.Errorf("error getting GPIO pin, pin %v not found", bcom) 148 | } 149 | // configure the pin to be an output if it is not already an output 150 | if pin.configuration != GPIOOutput { 151 | // first if the pin was configured for pwm, we should turn off the pwm 152 | if pin.pwmEnabled { 153 | res := C.set_PWM_dutycycle(pi.piID, C.uint(pin.pin), C.uint(0)) 154 | if res != 0 { 155 | return errors.Errorf("pwm set fail %d", res) 156 | } 157 | pin.pwmEnabled = false 158 | } 159 | res := C.set_mode(pi.piID, C.uint(pin.pin), C.PI_OUTPUT) 160 | if res != 0 { 161 | return rpiutils.ConvertErrorCodeToMessage(int(res), "failed to set mode") 162 | } 163 | } 164 | 165 | v := 0 166 | if high { 167 | v = 1 168 | } 169 | C.gpio_write(pi.piID, C.uint(pin.pin), C.uint(v)) 170 | 171 | return nil 172 | } 173 | 174 | func (pi *piPigpio) pwmBcom(bcom int) (float64, error) { 175 | // verify we are currently managing this pin via GPIOPinByName or reconfigure 176 | pin, ok := pi.gpioPins[bcom] 177 | if !ok { 178 | return 0, fmt.Errorf("error getting GPIO pin, pin %v not found", bcom) 179 | } 180 | if !pin.pwmEnabled { 181 | pi.logger.Debugf("pin %v is currently not configured as pwm", bcom) 182 | return 0, nil 183 | } 184 | res := C.get_PWM_dutycycle(pi.piID, C.uint(pin.pin)) 185 | return float64(res) / 255, nil 186 | } 187 | 188 | // SetPWMBcom sets the given broadcom pin to the given PWM duty cycle. 189 | func (pi *piPigpio) SetPWMBcom(bcom int, dutyCyclePct float64) error { 190 | pi.mu.Lock() 191 | defer pi.mu.Unlock() 192 | pin, ok := pi.gpioPins[bcom] 193 | if !ok { 194 | return fmt.Errorf("error getting GPIO pin, pin %v not found", bcom) 195 | } 196 | 197 | dutyCycle := rdkutils.ScaleByPct(255, dutyCyclePct) 198 | res := C.set_PWM_dutycycle(pi.piID, C.uint(pin.pin), C.uint(dutyCycle)) 199 | if res != 0 { 200 | return errors.Errorf("pwm set fail %d", res) 201 | } 202 | pin.configuration = GPIOPWM 203 | pin.pwmEnabled = true 204 | return nil 205 | } 206 | 207 | func (pi *piPigpio) pwmFreqBcom(bcom int) (uint, error) { 208 | res := C.get_PWM_frequency(pi.piID, C.uint(bcom)) 209 | return uint(res), nil 210 | } 211 | 212 | // SetPWMFreqBcom sets the given broadcom pin to the given PWM frequency. 213 | func (pi *piPigpio) SetPWMFreqBcom(bcom int, freqHz uint) error { 214 | pi.mu.Lock() 215 | defer pi.mu.Unlock() 216 | if freqHz == 0 { 217 | freqHz = rpiutils.DefaultPWMFreqHz 218 | } 219 | newRes := C.set_PWM_frequency(pi.piID, C.uint(bcom), C.uint(freqHz)) 220 | 221 | if newRes == C.PI_BAD_USER_GPIO { 222 | return rpiutils.ConvertErrorCodeToMessage(int(newRes), "pwm set freq failed") 223 | } 224 | 225 | if newRes != C.int(freqHz) { 226 | pi.logger.Infof("cannot set pwm freq to %d, setting to closest freq %d", freqHz, newRes) 227 | } 228 | return nil 229 | } 230 | -------------------------------------------------------------------------------- /utils/file_helpers_test.go: -------------------------------------------------------------------------------- 1 | package rpiutils 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "go.viam.com/rdk/logging" 9 | "go.viam.com/test" 10 | ) 11 | 12 | // TestI2CConfiguration tests the turn_i2c_on behavior. 13 | func TestI2CConfiguration(t *testing.T) { 14 | logger := logging.NewTestLogger(t) 15 | 16 | testCases := []struct { 17 | name string 18 | turnI2COn bool 19 | expectChange bool 20 | initialConfig string 21 | initialModule string 22 | }{ 23 | { 24 | name: "turn_on_from_scratch", 25 | turnI2COn: true, 26 | expectChange: true, 27 | initialConfig: "", 28 | initialModule: "", 29 | }, 30 | { 31 | name: "turn_on_already_enabled", 32 | turnI2COn: true, 33 | expectChange: false, 34 | initialConfig: "dtparam=i2c_arm=on\n", 35 | initialModule: "i2c-dev\n", 36 | }, 37 | { 38 | name: "turn_on_from_commented", 39 | turnI2COn: true, 40 | expectChange: true, 41 | initialConfig: "#dtparam=i2c_arm=on\n", 42 | initialModule: "#i2c-dev\n", 43 | }, 44 | { 45 | name: "false_does_nothing_empty", 46 | turnI2COn: false, 47 | expectChange: false, 48 | initialConfig: "", 49 | initialModule: "", 50 | }, 51 | { 52 | name: "false_does_nothing_enabled", 53 | turnI2COn: false, 54 | expectChange: false, 55 | initialConfig: "dtparam=i2c_arm=on\n", 56 | initialModule: "i2c-dev\n", 57 | }, 58 | { 59 | name: "false_does_nothing_disabled", 60 | turnI2COn: false, 61 | expectChange: false, 62 | initialConfig: "dtparam=i2c_arm=off\n", 63 | initialModule: "#i2c-dev\n", 64 | }, 65 | } 66 | 67 | for _, tc := range testCases { 68 | t.Run(tc.name, func(t *testing.T) { 69 | tempDir := t.TempDir() 70 | configPath := filepath.Join(tempDir, "config.txt") 71 | modulePath := filepath.Join(tempDir, "modules") 72 | 73 | if err := os.WriteFile(configPath, []byte(tc.initialConfig), 0644); err != nil { 74 | t.Fatal(err) 75 | } 76 | if err := os.WriteFile(modulePath, []byte(tc.initialModule), 0644); err != nil { 77 | t.Fatal(err) 78 | } 79 | 80 | // Test the I2C configuration flow 81 | var configChanged, moduleChanged bool 82 | var err error 83 | 84 | if tc.turnI2COn { 85 | configChanged, err = UpdateConfigFile(configPath, "dtparam=i2c_arm", "on", logger) 86 | test.That(t, err, test.ShouldBeNil) 87 | 88 | moduleChanged, err = UpdateModuleFile(modulePath, "i2c-dev", true, logger) 89 | test.That(t, err, test.ShouldBeNil) 90 | 91 | // Verify final state - should be enabled 92 | finalConfig, err := os.ReadFile(configPath) 93 | test.That(t, err, test.ShouldBeNil) 94 | test.That(t, string(finalConfig), test.ShouldContainSubstring, "dtparam=i2c_arm=on") 95 | 96 | finalModule, err := os.ReadFile(modulePath) 97 | test.That(t, err, test.ShouldBeNil) 98 | test.That(t, string(finalModule), test.ShouldContainSubstring, "i2c-dev") 99 | test.That(t, string(finalModule), test.ShouldNotContainSubstring, "#i2c-dev") 100 | } else { 101 | // When turn_i2c_on is false, no operations should occur 102 | configChanged = false 103 | moduleChanged = false 104 | 105 | // Verify no changes were made to files 106 | finalConfig, err := os.ReadFile(configPath) 107 | test.That(t, err, test.ShouldBeNil) 108 | test.That(t, string(finalConfig), test.ShouldEqual, tc.initialConfig) 109 | 110 | finalModule, err := os.ReadFile(modulePath) 111 | test.That(t, err, test.ShouldBeNil) 112 | test.That(t, string(finalModule), test.ShouldEqual, tc.initialModule) 113 | } 114 | 115 | shouldReboot := configChanged || moduleChanged 116 | test.That(t, shouldReboot, test.ShouldEqual, tc.expectChange) 117 | }) 118 | } 119 | } 120 | 121 | // TestI2CConfigIntegration tests integration with the board config. 122 | func TestI2CConfigIntegration(t *testing.T) { 123 | testCases := []struct { 124 | name string 125 | config Config 126 | expectCalls bool 127 | }{ 128 | { 129 | name: "turn_i2c_on_true", 130 | config: Config{ 131 | BoardSettings: BoardSettings{ 132 | TurnI2COn: true, 133 | }, 134 | }, 135 | expectCalls: true, 136 | }, 137 | { 138 | name: "turn_i2c_on_false", 139 | config: Config{ 140 | BoardSettings: BoardSettings{ 141 | TurnI2COn: false, 142 | }, 143 | }, 144 | expectCalls: false, 145 | }, 146 | { 147 | name: "turn_i2c_on_omitted", 148 | config: Config{ 149 | BoardSettings: BoardSettings{}, 150 | }, 151 | expectCalls: false, 152 | }, 153 | } 154 | 155 | for _, tc := range testCases { 156 | t.Run(tc.name, func(t *testing.T) { 157 | // Test that the logic correctly interprets the config 158 | shouldEnable := tc.config.BoardSettings.TurnI2COn 159 | test.That(t, shouldEnable, test.ShouldEqual, tc.expectCalls) 160 | }) 161 | } 162 | } 163 | 164 | // TestI2CEdgeCases tests edge cases for the I2C configuration. 165 | func TestI2CEdgeCases(t *testing.T) { 166 | logger := logging.NewTestLogger(t) 167 | 168 | t.Run("enable_with_existing_disabled_config", func(t *testing.T) { 169 | tempDir := t.TempDir() 170 | configPath := filepath.Join(tempDir, "config.txt") 171 | modulePath := filepath.Join(tempDir, "modules") 172 | 173 | // Start with I2C explicitly disabled 174 | initialConfig := "dtparam=i2c_arm=off\nother=setting\n" 175 | initialModule := "snd-bcm2835\n#i2c-dev\nother-module\n" 176 | 177 | if err := os.WriteFile(configPath, []byte(initialConfig), 0644); err != nil { 178 | t.Fatal(err) 179 | } 180 | if err := os.WriteFile(modulePath, []byte(initialModule), 0644); err != nil { 181 | t.Fatal(err) 182 | } 183 | 184 | // Enable I2C 185 | configChanged, err := UpdateConfigFile(configPath, "dtparam=i2c_arm", "on", logger) 186 | test.That(t, err, test.ShouldBeNil) 187 | test.That(t, configChanged, test.ShouldBeTrue) 188 | 189 | moduleChanged, err := UpdateModuleFile(modulePath, "i2c-dev", true, logger) 190 | test.That(t, err, test.ShouldBeNil) 191 | test.That(t, moduleChanged, test.ShouldBeTrue) 192 | 193 | // Verify final state 194 | finalConfig, err := os.ReadFile(configPath) 195 | test.That(t, err, test.ShouldBeNil) 196 | test.That(t, string(finalConfig), test.ShouldContainSubstring, "dtparam=i2c_arm=on") 197 | test.That(t, string(finalConfig), test.ShouldContainSubstring, "other=setting") 198 | 199 | finalModule, err := os.ReadFile(modulePath) 200 | test.That(t, err, test.ShouldBeNil) 201 | test.That(t, string(finalModule), test.ShouldContainSubstring, "snd-bcm2835") 202 | test.That(t, string(finalModule), test.ShouldContainSubstring, "i2c-dev") 203 | test.That(t, string(finalModule), test.ShouldContainSubstring, "other-module") 204 | test.That(t, string(finalModule), test.ShouldNotContainSubstring, "#i2c-dev") 205 | }) 206 | 207 | t.Run("enable_idempotent", func(t *testing.T) { 208 | tempDir := t.TempDir() 209 | configPath := filepath.Join(tempDir, "config.txt") 210 | modulePath := filepath.Join(tempDir, "modules") 211 | 212 | // Start with I2C already enabled 213 | initialConfig := "dtparam=i2c_arm=on\n" 214 | initialModule := "i2c-dev\n" 215 | 216 | if err := os.WriteFile(configPath, []byte(initialConfig), 0644); err != nil { 217 | t.Fatal(err) 218 | } 219 | if err := os.WriteFile(modulePath, []byte(initialModule), 0644); err != nil { 220 | t.Fatal(err) 221 | } 222 | 223 | // Try to enable again - should be no-op 224 | configChanged, err := UpdateConfigFile(configPath, "dtparam=i2c_arm", "on", logger) 225 | test.That(t, err, test.ShouldBeNil) 226 | test.That(t, configChanged, test.ShouldBeFalse) 227 | 228 | moduleChanged, err := UpdateModuleFile(modulePath, "i2c-dev", true, logger) 229 | test.That(t, err, test.ShouldBeNil) 230 | test.That(t, moduleChanged, test.ShouldBeFalse) 231 | 232 | // Verify no reboot needed 233 | shouldReboot := configChanged || moduleChanged 234 | test.That(t, shouldReboot, test.ShouldBeFalse) 235 | }) 236 | } 237 | -------------------------------------------------------------------------------- /rpi/board_test.go: -------------------------------------------------------------------------------- 1 | package rpi 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "testing" 7 | "time" 8 | 9 | rpiservo "raspberry-pi/rpi-servo" 10 | rpiutils "raspberry-pi/utils" 11 | 12 | "go.viam.com/rdk/components/servo" 13 | "go.viam.com/rdk/logging" 14 | "go.viam.com/rdk/resource" 15 | "go.viam.com/test" 16 | ) 17 | 18 | func TestPiPigpio(t *testing.T) { 19 | ctx := context.Background() 20 | logger := logging.NewTestLogger(t) 21 | 22 | cfg := rpiutils.Config{ 23 | Pins: []rpiutils.PinConfig{ 24 | {Name: "i1", Pin: "11", Type: "interrupt"}, // bcom 17 25 | {Name: "servo-i", Pin: "22", Type: "interrupt"}, 26 | {Name: "blue", Pin: "33", Type: "gpio"}, 27 | }, 28 | } 29 | resourceConfig := resource.Config{ 30 | Name: "foo", 31 | ConvertedAttributes: &cfg, 32 | } 33 | 34 | pp, err := newPigpio(ctx, nil, resourceConfig, logger) 35 | if os.Getuid() != 0 || err != nil && err.Error() == "not running on a pi" { 36 | t.Skip("not running as root on a pi") 37 | return 38 | } 39 | test.That(t, err, test.ShouldBeNil) 40 | 41 | p := pp.(*piPigpio) 42 | 43 | defer func() { 44 | err := p.Close(ctx) 45 | test.That(t, err, test.ShouldBeNil) 46 | }() 47 | 48 | t.Run("test interrupts on reconfigure", func(t *testing.T) { 49 | expectedNumInterrupts := 2 50 | // before adding an interrupt using DigitalInterruptByName, confirm 51 | // pi.interrupts length matches the number of interrupts in the config 52 | test.That(t, len(p.interrupts), test.ShouldEqual, expectedNumInterrupts) 53 | 54 | // add a digital interrupt using DigitalInterruptByName, 55 | // check that it has been added to the pi.interrupts list 56 | _, err := p.DigitalInterruptByName("10") 57 | expectedNumInterrupts = 3 58 | test.That(t, err, test.ShouldBeNil) 59 | test.That(t, len(p.interrupts), test.ShouldEqual, expectedNumInterrupts) 60 | 61 | // test that the number of interrupts is the same after reconfigure 62 | err = p.Reconfigure(ctx, nil, resourceConfig) 63 | test.That(t, err, test.ShouldBeNil) 64 | test.That(t, len(p.interrupts), test.ShouldEqual, expectedNumInterrupts) 65 | }) 66 | 67 | t.Run("gpio and pwm", func(t *testing.T) { 68 | pin, err := p.GPIOPinByName("29") 69 | test.That(t, err, test.ShouldBeNil) 70 | 71 | // try to set high 72 | err = pin.Set(ctx, true, nil) 73 | test.That(t, err, test.ShouldBeNil) 74 | 75 | v, err := pin.Get(ctx, nil) 76 | test.That(t, err, test.ShouldBeNil) 77 | test.That(t, v, test.ShouldEqual, true) 78 | 79 | // try to set low 80 | err = pin.Set(ctx, false, nil) 81 | test.That(t, err, test.ShouldBeNil) 82 | 83 | v, err = pin.Get(ctx, nil) 84 | test.That(t, err, test.ShouldBeNil) 85 | test.That(t, v, test.ShouldEqual, false) 86 | 87 | // pwm 50% 88 | err = pin.SetPWM(ctx, 0.5, nil) 89 | test.That(t, err, test.ShouldBeNil) 90 | 91 | vF, err := pin.PWM(ctx, nil) 92 | test.That(t, err, test.ShouldBeNil) 93 | test.That(t, vF, test.ShouldAlmostEqual, 0.5, 0.01) 94 | 95 | // 4000 hz 96 | err = pin.SetPWMFreq(ctx, 4000, nil) 97 | test.That(t, err, test.ShouldBeNil) 98 | 99 | vI, err := pin.PWMFreq(ctx, nil) 100 | test.That(t, err, test.ShouldBeNil) 101 | test.That(t, vI, test.ShouldEqual, 4000) 102 | 103 | // 90% 104 | err = pin.SetPWM(ctx, 0.9, nil) 105 | test.That(t, err, test.ShouldBeNil) 106 | 107 | vF, err = pin.PWM(ctx, nil) 108 | test.That(t, err, test.ShouldBeNil) 109 | test.That(t, vF, test.ShouldAlmostEqual, 0.9, 0.01) 110 | 111 | // 8000hz 112 | err = pin.SetPWMFreq(ctx, 8000, nil) 113 | test.That(t, err, test.ShouldBeNil) 114 | 115 | vI, err = pin.PWMFreq(ctx, nil) 116 | test.That(t, err, test.ShouldBeNil) 117 | test.That(t, vI, test.ShouldEqual, 8000) 118 | }) 119 | 120 | t.Run("gpio pins respect names and can use hardware name", func(t *testing.T) { 121 | // blue, pin33, and io13 are all the same pin 122 | pinBlue, err := p.GPIOPinByName("blue") 123 | test.That(t, err, test.ShouldBeNil) 124 | pin33, err := p.GPIOPinByName("33") 125 | test.That(t, err, test.ShouldBeNil) 126 | pinIO13, err := p.GPIOPinByName("io13") 127 | test.That(t, err, test.ShouldBeNil) 128 | // pin 35 is a different pin 129 | diffPin, err := p.GPIOPinByName("35") 130 | test.That(t, err, test.ShouldBeNil) 131 | 132 | // try to set high 133 | err = pinBlue.Set(ctx, true, nil) 134 | test.That(t, err, test.ShouldBeNil) 135 | 136 | v, err := pinBlue.Get(ctx, nil) 137 | test.That(t, err, test.ShouldBeNil) 138 | test.That(t, v, test.ShouldEqual, true) 139 | 140 | v, err = pin33.Get(ctx, nil) 141 | test.That(t, err, test.ShouldBeNil) 142 | test.That(t, v, test.ShouldEqual, true) 143 | 144 | v, err = pinIO13.Get(ctx, nil) 145 | test.That(t, err, test.ShouldBeNil) 146 | test.That(t, v, test.ShouldEqual, true) 147 | 148 | v, err = diffPin.Get(ctx, nil) 149 | test.That(t, err, test.ShouldBeNil) 150 | test.That(t, v, test.ShouldEqual, false) 151 | }) 152 | 153 | // interrupt is configured on pi board creation 154 | t.Run("preconfigured basic interrupt test", func(t *testing.T) { 155 | // Test interrupt i1 on pin 11 (bcom 17) 156 | i1, err := p.DigitalInterruptByName("i1") 157 | test.That(t, err, test.ShouldBeNil) 158 | 159 | err = p.SetGPIOBcom(17, false) 160 | test.That(t, err, test.ShouldBeNil) 161 | 162 | time.Sleep(5 * time.Millisecond) 163 | 164 | before, err := i1.Value(context.Background(), nil) 165 | test.That(t, err, test.ShouldBeNil) 166 | 167 | err = p.SetGPIOBcom(17, true) 168 | test.That(t, err, test.ShouldBeNil) 169 | 170 | time.Sleep(5 * time.Millisecond) 171 | 172 | after, err := i1.Value(context.Background(), nil) 173 | test.That(t, err, test.ShouldBeNil) 174 | test.That(t, after-before, test.ShouldEqual, int64(1)) 175 | }) 176 | 177 | // digital interrupt creates by name (on valid pin) 178 | t.Run("create new basic interrupt test", func(t *testing.T) { 179 | // Set and create interrupt on pin 13 180 | i2, err := p.DigitalInterruptByName("13") 181 | test.That(t, err, test.ShouldBeNil) 182 | // Set pin 13 (bcom 27) to LOW 183 | err = p.SetGPIOBcom(27, false) 184 | test.That(t, err, test.ShouldBeNil) 185 | 186 | time.Sleep(5 * time.Millisecond) 187 | 188 | // interrupt not created, bad pin name 189 | _, err = p.DigitalInterruptByName("some") 190 | test.That(t, err, test.ShouldNotBeNil) 191 | 192 | before, err := i2.Value(context.Background(), nil) 193 | test.That(t, err, test.ShouldBeNil) 194 | 195 | // Set pin 13 (bcom 27) to HIGH 196 | err = p.SetGPIOBcom(27, true) 197 | test.That(t, err, test.ShouldBeNil) 198 | 199 | time.Sleep(5 * time.Millisecond) 200 | 201 | after, err := i2.Value(context.Background(), nil) 202 | test.That(t, err, test.ShouldBeNil) 203 | test.That(t, after-before, test.ShouldEqual, int64(1)) 204 | 205 | _, err = p.DigitalInterruptByName("11") 206 | test.That(t, err, test.ShouldBeNil) 207 | }) 208 | 209 | // test servo movement and digital interrupt 210 | // this function is within rpi in order to access piPigpio 211 | t.Run("servo in/out", func(t *testing.T) { 212 | servoReg, ok := resource.LookupRegistration(servo.API, rpiservo.Model) 213 | test.That(t, ok, test.ShouldBeTrue) 214 | test.That(t, servoReg, test.ShouldNotBeNil) 215 | servoInt, err := servoReg.Constructor( 216 | ctx, 217 | nil, 218 | resource.Config{ 219 | Name: "servo", 220 | ConvertedAttributes: &rpiservo.ServoConfig{Pin: "22"}, 221 | }, 222 | logger, 223 | ) 224 | test.That(t, err, test.ShouldBeNil) 225 | servo1 := servoInt.(servo.Servo) 226 | 227 | // Move to 90 deg and check position 228 | err = servo1.Move(ctx, 90, nil) 229 | test.That(t, err, test.ShouldBeNil) 230 | 231 | v, err := servo1.Position(ctx, nil) 232 | test.That(t, err, test.ShouldBeNil) 233 | test.That(t, int(v), test.ShouldEqual, 90) 234 | 235 | // should move to max position even though 190 is out of range 236 | err = servo1.Move(ctx, 190, nil) 237 | test.That(t, err, test.ShouldBeNil) 238 | 239 | v, err = servo1.Position(ctx, nil) 240 | test.That(t, err, test.ShouldBeNil) 241 | test.That(t, int(v), test.ShouldEqual, 180) 242 | 243 | time.Sleep(300 * time.Millisecond) 244 | 245 | servoI, err := p.DigitalInterruptByName("servo-i") 246 | test.That(t, err, test.ShouldBeNil) 247 | val, err := servoI.Value(context.Background(), nil) 248 | test.That(t, err, test.ShouldBeNil) 249 | test.That(t, val, test.ShouldAlmostEqual, int64(2500), 100) // this is a tad noisy 250 | 251 | // Next position (120 deg) 252 | err = servo1.Move(ctx, 120, nil) 253 | test.That(t, err, test.ShouldBeNil) 254 | 255 | v, err = servo1.Position(ctx, nil) 256 | test.That(t, err, test.ShouldBeNil) 257 | test.That(t, int(v), test.ShouldEqual, 120) 258 | 259 | time.Sleep(300 * time.Millisecond) 260 | val, err = servoI.Value(context.Background(), nil) 261 | test.That(t, err, test.ShouldBeNil) 262 | test.That(t, val, test.ShouldAlmostEqual, int64(1833), 50) // this is a tad noisy 263 | }) 264 | } 265 | -------------------------------------------------------------------------------- /utils/errors.go: -------------------------------------------------------------------------------- 1 | // Package rpiutils contains implementations to convert error codes to human readable format. 2 | package rpiutils 3 | 4 | import ( 5 | "fmt" 6 | ) 7 | 8 | // PiGPIOErrorMap maps the error codes to the human readable error names. This can be found at the pigpio C interface. 9 | var PiGPIOErrorMap = map[int]string{ 10 | -1: "PI_INIT_FAILED: gpioInitialise failed", 11 | -2: "PI_BAD_USER_GPIO: GPIO not 0-31", 12 | -3: "PI_BAD_GPIO: GPIO not 0-53", 13 | -4: "PI_BAD_MODE: mode not 0-7", 14 | -5: "PI_BAD_LEVEL: level not 0-1", 15 | -6: "PI_BAD_PUD: pud not 0-2", 16 | -7: "PI_BAD_PULSEWIDTH: pulsewidth not 0 or 500-2500", 17 | -8: "PI_BAD_DUTYCYCLE: dutycycle outside set range", 18 | -9: "PI_BAD_TIMER: timer not 0-9", 19 | -10: "PI_BAD_MS: ms not 10-60000", 20 | -11: "PI_BAD_TIMETYPE: timetype not 0-1", 21 | -12: "PI_BAD_SECONDS: seconds < 0", 22 | -13: "PI_BAD_MICROS: micros not 0-999999", 23 | -14: "PI_TIMER_FAILED: gpioSetTimerFunc failed", 24 | -15: "PI_BAD_WDOG_TIMEOUT: timeout not 0-60000", 25 | -16: "PI_NO_ALERT_FUNC: DEPRECATED", 26 | -17: "PI_BAD_CLK_PERIPH: clock peripheral not 0-1", 27 | -18: "PI_BAD_CLK_SOURCE: DEPRECATED", 28 | -19: "PI_BAD_CLK_MICROS: clock micros not 1, 2, 4, 5, 8, or 10", 29 | -20: "PI_BAD_BUF_MILLIS: buf millis not 100-10000", 30 | -21: "PI_BAD_DUTYRANGE: dutycycle range not 25-40000", 31 | -22: "PI_BAD_SIGNUM: signum not 0-63", 32 | -23: "PI_BAD_PATHNAME: can't open pathname", 33 | -24: "PI_NO_HANDLE: no handle available", 34 | -25: "PI_BAD_HANDLE: unknown handle", 35 | -26: "PI_BAD_IF_FLAGS: ifFlags > 4", 36 | -27: "PI_BAD_CHANNEL_OR_PI_BAD_PRIM_CHANNEL: DMA channel not 0-15 OR DMA primary channel not 0-15", 37 | -28: "PI_BAD_SOCKET_PORT: socket port not 1024-32000", 38 | -29: "PI_BAD_FIFO_COMMAND: unrecognized fifo command", 39 | -30: "PI_BAD_SECO_CHANNEL: DMA secondary channel not 0-15", 40 | -31: "PI_NOT_INITIALISED: function called before gpioInitialise", 41 | -32: "PI_INITIALISED: function called after gpioInitialise", 42 | -33: "PI_BAD_WAVE_MODE: waveform mode not 0-3", 43 | -34: "PI_BAD_CFG_INTERNAL: bad parameter in gpioCfgInternals call", 44 | -35: "PI_BAD_WAVE_BAUD: baud rate not 50-250K(RX)/50-1M(TX)", 45 | -36: "PI_TOO_MANY_PULSES: waveform has too many pulses", 46 | -37: "PI_TOO_MANY_CHARS: waveform has too many chars", 47 | -38: "PI_NOT_SERIAL_GPIO: no bit bang serial read on GPIO", 48 | -39: "PI_BAD_SERIAL_STRUC: bad (null) serial structure parameter", 49 | -40: "PI_BAD_SERIAL_BUF: bad (null) serial buf parameter", 50 | -41: "PI_NOT_PERMITTED: GPIO operation not permitted", 51 | -42: "PI_SOME_PERMITTED: one or more GPIO not permitted", 52 | -43: "PI_BAD_WVSC_COMMND: bad WVSC subcommand", 53 | -44: "PI_BAD_WVSM_COMMND: bad WVSM subcommand", 54 | -45: "PI_BAD_WVSP_COMMND: bad WVSP subcommand", 55 | -46: "PI_BAD_PULSELEN: trigger pulse length not 1-100", 56 | -47: "PI_BAD_SCRIPT: invalid script", 57 | -48: "PI_BAD_SCRIPT_ID: unknown script id", 58 | -49: "PI_BAD_SER_OFFSET: add serial data offset > 30 minutes", 59 | -50: "PI_GPIO_IN_USE: GPIO already in use", 60 | -51: "PI_BAD_SERIAL_COUNT: must read at least a byte at a time", 61 | -52: "PI_BAD_PARAM_NUM: script parameter id not 0-9", 62 | -53: "PI_DUP_TAG: script has duplicate tag", 63 | -54: "PI_TOO_MANY_TAGS: script has too many tags", 64 | -55: "PI_BAD_SCRIPT_CMD: illegal script command", 65 | -56: "PI_BAD_VAR_NUM: script variable id not 0-149", 66 | -57: "PI_NO_SCRIPT_ROOM: no more room for scripts", 67 | -58: "PI_NO_MEMORY: can't allocate temporary memory", 68 | -59: "PI_SOCK_READ_FAILED: socket read failed", 69 | -60: "PI_SOCK_WRIT_FAILED: socket write failed", 70 | -61: "PI_TOO_MANY_PARAM: too many script parameters (> 10)", 71 | -62: "PI_SCRIPT_NOT_READY: script initialising", 72 | -63: "PI_BAD_TAG: script has unresolved tag", 73 | -64: "PI_BAD_MICS_DELAY: bad MICS delay (too large)", 74 | -65: "PI_BAD_MILS_DELAY: bad MILS delay (too large)", 75 | -66: "PI_BAD_WAVE_ID: non existent wave id", 76 | -67: "PI_TOO_MANY_CBS: No more CBs for waveform", 77 | -68: "PI_TOO_MANY_OOL: No more OOL for waveform", 78 | -69: "PI_EMPTY_WAVEFORM: attempt to create an empty waveform", 79 | -70: "PI_NO_WAVEFORM_ID: no more waveforms", 80 | -71: "PI_I2C_OPEN_FAILED: can't open I2C device", 81 | -72: "PI_SER_OPEN_FAILED: can't open serial device", 82 | -73: "PI_SPI_OPEN_FAILED: can't open SPI device", 83 | -74: "PI_BAD_I2C_BUS: bad I2C bus", 84 | -75: "PI_BAD_I2C_ADDR: bad I2C address", 85 | -76: "PI_BAD_SPI_CHANNEL: bad SPI channel", 86 | -77: "PI_BAD_FLAGS: bad i2c/spi/ser open flags", 87 | -78: "PI_BAD_SPI_SPEED: bad SPI speed", 88 | -79: "PI_BAD_SER_DEVICE: bad serial device name", 89 | -80: "PI_BAD_SER_SPEED: bad serial baud rate", 90 | -81: "PI_BAD_PARAM: bad i2c/spi/ser parameter", 91 | -82: "PI_I2C_WRITE_FAILED: i2c write failed", 92 | -83: "PI_I2C_READ_FAILED: i2c read failed", 93 | -84: "PI_BAD_SPI_COUNT: bad SPI count", 94 | -85: "PI_SER_WRITE_FAILED: ser write failed", 95 | -86: "PI_SER_READ_FAILED: ser read failed", 96 | -87: "PI_SER_READ_NO_DATA: ser read no data available", 97 | -88: "PI_UNKNOWN_COMMAND: unknown command", 98 | -89: "PI_SPI_XFER_FAILED: spi xfer/read/write failed", 99 | -90: "PI_BAD_POINTER: bad (NULL) pointer", 100 | -91: "PI_NO_AUX_SPI: no auxiliary SPI on Pi A or B", 101 | -92: "PI_NOT_PWM_GPIO: GPIO is not in use for PWM", 102 | -93: "PI_NOT_SERVO_GPI: GPIO is not in use for servo pulses", 103 | -94: "PI_NOT_HCLK_GPIO: GPIO has no hardware clock", 104 | -95: "PI_NOT_HPWM_GPIO: GPIO has no hardware PWM", 105 | -96: "PI_BAD_HPWM_FREQ: invalid hardware PWM frequency", 106 | -97: "PI_BAD_HPWM_DUTY: hardware PWM dutycycle not 0-1M", 107 | -98: "PI_BAD_HCLK_FREQ: invalid hardware clock frequency", 108 | -99: "PI_BAD_HCLK_PASS: need password to use hardware clock 1", 109 | -100: "PI_HPWM_ILLEGAL: illegal, PWM in use for main clock", 110 | -101: "PI_BAD_DATABITS: serial data bits not 1-32", 111 | -102: "PI_BAD_STOPBITS: serial (half) stop bits not 2-8", 112 | -103: "PI_MSG_TOOBIG: socket/pipe message too big", 113 | -104: "PI_BAD_MALLOC_MODE: bad memory allocation mode", 114 | -105: "PI_TOO_MANY_SEGS: too many I2C transaction segments", 115 | -106: "PI_BAD_I2C_SEG: an I2C transaction segment failed", 116 | -107: "PI_BAD_SMBUS_CMD: SMBus command not supported by driver", 117 | -108: "PI_NOT_I2C_GPIO: no bit bang I2C in progress on GPIO", 118 | -109: "PI_BAD_I2C_WLEN: bad I2C write length", 119 | -110: "PI_BAD_I2C_RLEN: bad I2C read length", 120 | -111: "PI_BAD_I2C_CMD: bad I2C command", 121 | -112: "PI_BAD_I2C_BAUD: bad I2C baud rate, not 50-500k", 122 | -113: "PI_CHAIN_LOOP_CNT: bad chain loop count", 123 | -114: "PI_BAD_CHAIN_LOOP: empty chain loop", 124 | -115: "PI_CHAIN_COUNTER: too many chain counters", 125 | -116: "PI_BAD_CHAIN_CMD: bad chain command", 126 | -117: "PI_BAD_CHAIN_DELAY: bad chain delay micros", 127 | -118: "PI_CHAIN_NESTING: chain counters nested too deeply", 128 | -119: "PI_CHAIN_TOO_BIG: chain is too long", 129 | -120: "PI_DEPRECATED: deprecated function removed", 130 | -121: "PI_BAD_SER_INVERT: bit bang serial invert not 0 or 1", 131 | -122: "PI_BAD_EDGE: bad ISR edge value, not 0-2", 132 | -123: "PI_BAD_ISR_INIT: bad ISR initialisation", 133 | -124: "PI_BAD_FOREVER: loop forever must be last command", 134 | -125: "PI_BAD_FILTER: bad filter parameter", 135 | -126: "PI_BAD_PAD: bad pad number", 136 | -127: "PI_BAD_STRENGTH: bad pad drive strength", 137 | -128: "PI_FIL_OPEN_FAILED: file open failed", 138 | -129: "PI_BAD_FILE_MODE: bad file mode", 139 | -130: "PI_BAD_FILE_FLAG: bad file flag", 140 | -131: "PI_BAD_FILE_READ: bad file read", 141 | -132: "PI_BAD_FILE_WRITE: bad file write", 142 | -133: "PI_FILE_NOT_ROPEN: file not open for read", 143 | -134: "PI_FILE_NOT_WOPEN: file not open for write", 144 | -135: "PI_BAD_FILE_SEEK: bad file seek", 145 | -136: "PI_NO_FILE_MATCH: no files match pattern", 146 | -137: "PI_NO_FILE_ACCESS: no permission to access file", 147 | -138: "PI_FILE_IS_A_DIR: file is a directory", 148 | -139: "PI_BAD_SHELL_STATUS: bad shell return status", 149 | -140: "PI_BAD_SCRIPT_NAME: bad script name", 150 | -141: "PI_BAD_SPI_BAUD: bad SPI baud rate, not 50-500k", 151 | -142: "PI_NOT_SPI_GPIO: no bit bang SPI in progress on GPIO", 152 | -143: "PI_BAD_EVENT_ID: bad event id", 153 | -144: "PI_CMD_INTERRUPTED: Used by Python", 154 | -145: "PI_NOT_ON_BCM2711: not available on BCM2711", 155 | -146: "PI_ONLY_ON_BCM2711: only available on BCM271", 156 | 157 | -2000: "PIGIF_BAD_SEND: could not send to pigpio", 158 | -2001: "PIGIF_BAD_RECV: no response to pigpio message", 159 | -2002: "PIGIF_BAD_GETADDRINFO: can't get socket address info for pigpio", 160 | -2003: "PIGIF_BAD_CONNECT: can't connect to pigpio", 161 | -2004: "PIGIF_BAD_SOCKET: can't create socket", 162 | -2005: "PIGIF_BAD_NOIB: can't find pigpio", 163 | -2006: "PIGIF_DUPLICATE_CALLBACK: callback already exists", 164 | -2007: "PIGIF_BAD_MALLOC: can't allocate memory", 165 | -2008: "PIGIF_BAD_CALLBACK: can't add callback", 166 | -2009: "PIGIF_NOTIFY_FAILED: can't notify pigpio", 167 | -2010: "PIGIF_CALLBACK_NOT_FOUND: callback not found", 168 | -2011: "PIGIF_UNCONNECTED_PI: can't find connected pi", 169 | -2012: "PIGIF_TOO_MANY_PIS: can't create more socket connections", 170 | 171 | -2099: "PI_PIGIF_ERR_99", 172 | -3000: "PI_CUSTOM_ERR_0", 173 | -3999: "PI_CUSTOM_ERR_999", 174 | } 175 | 176 | // ConvertErrorCodeToMessage converts error code to a human-readable string. 177 | func ConvertErrorCodeToMessage(errorCode int, message string) error { 178 | errorMessage, exists := PiGPIOErrorMap[errorCode] 179 | if exists { 180 | return fmt.Errorf("%s: %s", message, errorMessage) 181 | } 182 | return fmt.Errorf("%s: %d", message, errorCode) 183 | } 184 | 185 | // WrongModelErr informs the user when the model they configured for their pi is not correct. 186 | func WrongModelErr(wrongModel string) error { 187 | return fmt.Errorf("incorrect Raspberry Pi model detected, check that model %v is correct", wrongModel) 188 | } 189 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2024 Viam 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /rpi-servo/rpiservo_test.go: -------------------------------------------------------------------------------- 1 | package rpiservo 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "go.viam.com/rdk/components/board" 8 | "go.viam.com/rdk/components/servo" 9 | "go.viam.com/rdk/logging" 10 | "go.viam.com/rdk/operation" 11 | "go.viam.com/rdk/resource" 12 | "go.viam.com/test" 13 | "raspberry-pi/rpi" 14 | rpiutils "raspberry-pi/utils" 15 | ) 16 | 17 | func createDummyBoard(t *testing.T, ctx context.Context) board.Board { 18 | // create board dependency 19 | piReg, ok := resource.LookupRegistration(board.API, rpi.ModelPi4) 20 | test.That(t, ok, test.ShouldBeTrue) 21 | test.That(t, piReg, test.ShouldNotBeNil) 22 | 23 | piInt, err := piReg.Constructor( 24 | ctx, 25 | nil, 26 | resource.Config{ 27 | Name: "rpi", 28 | ConvertedAttributes: &rpiutils.Config{}, 29 | }, 30 | logging.NewTestLogger(t), 31 | ) 32 | 33 | test.That(t, err, test.ShouldBeNil) 34 | p := piInt.(board.Board) 35 | 36 | return p 37 | } 38 | 39 | func TestConstructor(t *testing.T) { 40 | logger := logging.NewTestLogger(t) 41 | ctx := context.Background() 42 | 43 | p := createDummyBoard(t, ctx) 44 | defer func() { 45 | err := p.Close(ctx) 46 | test.That(t, err, test.ShouldBeNil) 47 | }() 48 | 49 | t.Run("test local piPigpioServo struct fields", func(t *testing.T) { 50 | ctx := context.Background() 51 | servoReg, ok := resource.LookupRegistration(servo.API, Model) 52 | test.That(t, ok, test.ShouldBeTrue) 53 | test.That(t, servoReg, test.ShouldNotBeNil) 54 | 55 | initPos := 33.0 56 | servoInt, err := servoReg.Constructor( 57 | ctx, 58 | nil, 59 | resource.Config{ 60 | Name: "servo", 61 | ConvertedAttributes: &ServoConfig{Pin: "22", StartPos: &initPos, Freq: 100}, 62 | }, 63 | logger, 64 | ) 65 | test.That(t, err, test.ShouldBeNil) 66 | 67 | servo1 := servoInt.(servo.Servo) 68 | pos1, err := servo1.Position(ctx, nil) 69 | test.That(t, err, test.ShouldBeNil) 70 | test.That(t, pos1, test.ShouldEqual, 33) 71 | 72 | // test local fields and defaults 73 | testServo := servo1.(*piPigpioServo) 74 | test.That(t, testServo.holdPos, test.ShouldBeTrue) 75 | test.That(t, testServo.pwInUse, test.ShouldAlmostEqual, 866, 1) 76 | test.That(t, testServo.pwmFreqHz, test.ShouldEqual, 100) 77 | test.That(t, testServo.min, test.ShouldEqual, 0) 78 | test.That(t, testServo.max, test.ShouldEqual, 180) 79 | test.That(t, testServo.maxRotation, test.ShouldEqual, 180) 80 | test.That(t, testServo.pinname, test.ShouldEqual, "22") 81 | test.That(t, testServo.pin, test.ShouldEqual, 25) 82 | }) 83 | } 84 | 85 | func TestInitializationFunctions(t *testing.T) { 86 | ctx := context.Background() 87 | 88 | p := createDummyBoard(t, ctx) 89 | defer func() { 90 | err := p.Close(ctx) 91 | test.That(t, err, test.ShouldBeNil) 92 | }() 93 | 94 | t.Run("test servo initialization", func(t *testing.T) { 95 | logger := logging.NewTestLogger(t) 96 | bcom := uint(3) 97 | conf := resource.Config{ 98 | Name: "servo", 99 | } 100 | 101 | // invalid conf, maxRotation < min 102 | newConf := &ServoConfig{ 103 | Min: 200, 104 | Max: 180, 105 | MaxRotation: 180, 106 | } 107 | 108 | s, err := initializeServo(conf, logger, bcom, newConf) 109 | test.That(t, s, test.ShouldBeNil) 110 | test.That(t, err, test.ShouldNotBeNil) 111 | test.That(t, err.Error(), test.ShouldContainSubstring, "maxRotation is less than minimum") 112 | 113 | // invalid conf, maxRotation < max 114 | newConf = &ServoConfig{ 115 | Min: 0, 116 | Max: 180, 117 | MaxRotation: 179, 118 | } 119 | 120 | s, err = initializeServo(conf, logger, bcom, newConf) 121 | test.That(t, s, test.ShouldBeNil) 122 | test.That(t, err, test.ShouldNotBeNil) 123 | test.That(t, err.Error(), test.ShouldContainSubstring, "maxRotation is less than maximum") 124 | 125 | // valid conf 126 | newConf = &ServoConfig{ 127 | Min: 0, 128 | Max: 180, 129 | MaxRotation: 180, 130 | } 131 | 132 | targetPin := 3 133 | 134 | s, err = initializeServo(conf, logger, bcom, newConf) 135 | test.That(t, err, test.ShouldBeNil) 136 | test.That(t, s, test.ShouldNotBeNil) 137 | test.That(t, int(s.piID), test.ShouldBeGreaterThanOrEqualTo, 0) 138 | test.That(t, int(s.pin), test.ShouldEqual, targetPin) 139 | test.That(t, s.max, test.ShouldEqual, 180) 140 | test.That(t, s.min, test.ShouldEqual, 0) 141 | test.That(t, s.maxRotation, test.ShouldEqual, 180) 142 | 143 | // close pigpio 144 | s.Close(context.TODO()) 145 | }) 146 | 147 | t.Run("test setting initial position", func(t *testing.T) { 148 | logger := logging.NewTestLogger(t) 149 | bcom := uint(3) 150 | conf := resource.Config{ 151 | Name: "servo", 152 | } 153 | newConf := &ServoConfig{ 154 | Pin: "22", 155 | } 156 | 157 | // create servo 158 | s, err := initializeServo(conf, logger, bcom, newConf) 159 | test.That(t, err, test.ShouldBeNil) 160 | 161 | // default(nil) initial position 162 | err = setInitialPosition(s, &ServoConfig{StartPos: nil}) 163 | test.That(t, err, test.ShouldBeNil) 164 | 165 | // valid initial position 166 | initPos := 33.0 167 | err = setInitialPosition(s, &ServoConfig{StartPos: &initPos}) 168 | test.That(t, err, test.ShouldBeNil) 169 | 170 | // invalid pin 171 | s.pin = 10000 172 | err = setInitialPosition(s, &ServoConfig{StartPos: nil}) 173 | test.That(t, err, test.ShouldNotBeNil) 174 | test.That(t, err.Error(), test.ShouldContainSubstring, "PI_BAD_USER_GPIO") 175 | 176 | // invalid angle 177 | s.pin = 22 178 | initPos = 181.0 179 | err = setInitialPosition(s, &ServoConfig{StartPos: &initPos}) 180 | test.That(t, err, test.ShouldNotBeNil) 181 | test.That(t, err.Error(), test.ShouldContainSubstring, "invalid pulse width") 182 | 183 | // close pigpio 184 | s.Close(context.TODO()) 185 | }) 186 | 187 | t.Run("test handle hold position", func(t *testing.T) { 188 | logger := logging.NewTestLogger(t) 189 | bcom := uint(3) 190 | conf := resource.Config{ 191 | Name: "servo", 192 | } 193 | newConf := &ServoConfig{ 194 | Pin: "22", 195 | } 196 | 197 | // create servo 198 | s, err := initializeServo(conf, logger, bcom, newConf) 199 | test.That(t, err, test.ShouldBeNil) 200 | 201 | // default(nil) hold position is true 202 | handleHoldPosition(s, &ServoConfig{HoldPos: nil}) 203 | test.That(t, s.holdPos, test.ShouldBeTrue) 204 | 205 | // hold position is true 206 | holdPos := true 207 | handleHoldPosition(s, &ServoConfig{HoldPos: &holdPos}) 208 | test.That(t, s.holdPos, test.ShouldBeTrue) 209 | 210 | // hold position is false 211 | holdPos = false 212 | handleHoldPosition(s, &ServoConfig{HoldPos: &holdPos}) 213 | test.That(t, s.holdPos, test.ShouldBeFalse) 214 | 215 | // close pigpio 216 | s.Close(context.TODO()) 217 | }) 218 | } 219 | 220 | func TestServoFunctions(t *testing.T) { 221 | t.Run("test validate and set configuration", func(t *testing.T) { 222 | s := &piPigpioServo{} 223 | 224 | // invalid conf, maxRotation < min 225 | newConf := &ServoConfig{ 226 | Pin: "22", 227 | Min: 200, 228 | MaxRotation: 180, 229 | Max: 180, 230 | } 231 | 232 | err := s.validateAndSetConfiguration(newConf) 233 | test.That(t, err, test.ShouldNotBeNil) 234 | test.That(t, err.Error(), test.ShouldContainSubstring, "maxRotation is less than minimum") 235 | 236 | // invalid conf, maxRotation < max 237 | newConf = &ServoConfig{ 238 | Pin: "22", 239 | Min: 1, 240 | Max: 180, 241 | MaxRotation: 179, 242 | } 243 | 244 | err = s.validateAndSetConfiguration(newConf) 245 | test.That(t, err, test.ShouldNotBeNil) 246 | test.That(t, err.Error(), test.ShouldContainSubstring, "maxRotation is less than maximum") 247 | 248 | // valid conf 249 | newConf = &ServoConfig{ 250 | Pin: "22", 251 | Min: 0, 252 | MaxRotation: 1234, 253 | Max: 180, 254 | } 255 | 256 | err = s.validateAndSetConfiguration(newConf) 257 | test.That(t, err, test.ShouldBeNil) 258 | test.That(t, s.max, test.ShouldEqual, 180) 259 | test.That(t, s.min, test.ShouldEqual, 0) 260 | test.That(t, s.maxRotation, test.ShouldEqual, 1234) 261 | }) 262 | t.Run("test parse config", func(t *testing.T) { 263 | newConf := &ServoConfig{Pin: "100"} 264 | 265 | parsedConf, err := parseConfig( 266 | resource.Config{ConvertedAttributes: newConf}, 267 | ) 268 | 269 | test.That(t, err, test.ShouldBeNil) 270 | test.That(t, parsedConf, test.ShouldNotBeNil) 271 | test.That(t, parsedConf.Pin, test.ShouldEqual, "100") 272 | 273 | badConf := &rpiutils.Config{} 274 | parsedConf, err = parseConfig( 275 | resource.Config{ConvertedAttributes: badConf}, 276 | ) 277 | 278 | test.That(t, parsedConf, test.ShouldBeNil) 279 | // unexpected type, only kind of error 280 | test.That(t, err, test.ShouldNotBeNil) 281 | }) 282 | t.Run("test config validation", func(t *testing.T) { 283 | newConf := &ServoConfig{Pin: "22"} 284 | err := validateConfig(newConf) 285 | test.That(t, err, test.ShouldBeNil) 286 | 287 | newConf = &ServoConfig{Pin: ""} 288 | err = validateConfig(newConf) 289 | test.That(t, err.Error(), test.ShouldContainSubstring, "need pin for pi servo") 290 | }) 291 | 292 | t.Run("test get broadcom pin", func(t *testing.T) { 293 | // pin with special name/function 294 | bcom, err := getBroadcomPin("sclk") 295 | test.That(t, err, test.ShouldBeNil) 296 | test.That(t, bcom, test.ShouldEqual, 11) 297 | 298 | // standard pin 299 | bcom, err = getBroadcomPin("22") 300 | test.That(t, err, test.ShouldBeNil) 301 | test.That(t, bcom, test.ShouldEqual, 25) 302 | 303 | // pin based on IO 304 | bcom, err = getBroadcomPin("io21") 305 | test.That(t, err, test.ShouldBeNil) 306 | test.That(t, bcom, test.ShouldEqual, 21) 307 | 308 | // bad pin 309 | bcom, err = getBroadcomPin("bad") 310 | test.That(t, err.Error(), test.ShouldContainSubstring, "no hw mapping for bad") 311 | test.That(t, bcom, test.ShouldEqual, 0) 312 | }) 313 | 314 | t.Run("check servo math", func(t *testing.T) { 315 | pw := angleToPulseWidth(1, servoDefaultMaxRotation) 316 | test.That(t, pw, test.ShouldEqual, 511) 317 | pw = angleToPulseWidth(0, servoDefaultMaxRotation) 318 | test.That(t, pw, test.ShouldEqual, 500) 319 | pw = angleToPulseWidth(179, servoDefaultMaxRotation) 320 | test.That(t, pw, test.ShouldEqual, 2488) 321 | pw = angleToPulseWidth(180, servoDefaultMaxRotation) 322 | test.That(t, pw, test.ShouldEqual, 2500) 323 | pw = angleToPulseWidth(179, 270) 324 | test.That(t, pw, test.ShouldEqual, 1825) 325 | pw = angleToPulseWidth(180, 270) 326 | test.That(t, pw, test.ShouldEqual, 1833) 327 | a := pulseWidthToAngle(511, servoDefaultMaxRotation) 328 | test.That(t, a, test.ShouldEqual, 1) 329 | a = pulseWidthToAngle(500, servoDefaultMaxRotation) 330 | test.That(t, a, test.ShouldEqual, 0) 331 | a = pulseWidthToAngle(2500, servoDefaultMaxRotation) 332 | test.That(t, a, test.ShouldEqual, 180) 333 | a = pulseWidthToAngle(2488, servoDefaultMaxRotation) 334 | test.That(t, a, test.ShouldEqual, 179) 335 | a = pulseWidthToAngle(1825, 270) 336 | test.That(t, a, test.ShouldEqual, 179) 337 | a = pulseWidthToAngle(1833, 270) 338 | test.That(t, a, test.ShouldEqual, 180) 339 | }) 340 | 341 | t.Run(("check Move IsMoving and pigpio errors"), func(t *testing.T) { 342 | ctx := context.Background() 343 | s := &piPigpioServo{pinname: "1", maxRotation: 180, opMgr: operation.NewSingleOperationManager()} 344 | 345 | s.pwInUse = -93 346 | err := s.pigpioErrors(int(s.pwInUse)) 347 | test.That(t, err.Error(), test.ShouldContainSubstring, "pulsewidths") 348 | moving, err := s.IsMoving(ctx) 349 | test.That(t, moving, test.ShouldBeFalse) 350 | test.That(t, err, test.ShouldNotBeNil) 351 | 352 | s.pwInUse = -7 353 | err = s.pigpioErrors(int(s.pwInUse)) 354 | test.That(t, err.Error(), test.ShouldContainSubstring, "range") 355 | moving, err = s.IsMoving(ctx) 356 | test.That(t, moving, test.ShouldBeFalse) 357 | test.That(t, err, test.ShouldNotBeNil) 358 | 359 | s.pwInUse = 0 360 | err = s.pigpioErrors(int(s.pwInUse)) 361 | test.That(t, err, test.ShouldBeNil) 362 | moving, err = s.IsMoving(ctx) 363 | test.That(t, moving, test.ShouldBeFalse) 364 | test.That(t, err, test.ShouldBeNil) 365 | 366 | s.pwInUse = 1 367 | err = s.pigpioErrors(int(s.pwInUse)) 368 | test.That(t, err, test.ShouldBeNil) 369 | moving, err = s.IsMoving(ctx) 370 | test.That(t, moving, test.ShouldBeFalse) 371 | test.That(t, err, test.ShouldBeNil) 372 | 373 | err = s.pigpioErrors(-4) 374 | test.That(t, err.Error(), test.ShouldContainSubstring, "failed") 375 | moving, err = s.IsMoving(ctx) 376 | test.That(t, moving, test.ShouldBeFalse) 377 | test.That(t, err, test.ShouldBeNil) 378 | 379 | err = s.Move(ctx, 8, nil) 380 | test.That(t, err, test.ShouldNotBeNil) 381 | 382 | err = s.Stop(ctx, nil) 383 | test.That(t, err, test.ShouldNotBeNil) 384 | 385 | pos, err := s.Position(ctx, nil) 386 | test.That(t, err, test.ShouldNotBeNil) 387 | test.That(t, pos, test.ShouldEqual, 0) 388 | }) 389 | } 390 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [`raspberry-pi` module](https://app.viam.com/module/viam/raspberry-pi) 2 | 3 | This module implements the [`rdk:component:board` API](https://docs.viam.com/components/board/#api) and [`rdk:component:servo` API](https://docs.viam.com/components/servo/#api) 4 | 5 | This module provides the following models to access GPIO functionality (input, output, PWM, power, serial interfaces, etc.): 6 | 7 | * `viam:raspberry-pi:rpi5` - Configure a Raspberry Pi 5 board 8 | * `viam:raspberry-pi:rpi4` - Configure a Raspberry Pi 4 board 9 | * `viam:raspberry-pi:rpi3` - Configure a Raspberry Pi 3 board 10 | * `viam:raspberry-pi:rpi2` - Configure a Raspberry Pi 2 board 11 | * `viam:raspberry-pi:rpi1` - Configure a Raspberry Pi 1 board 12 | * `viam:raspberry-pi:rpi0` - Configure a Raspberry Pi 0 board 13 | * `viam:raspberry-pi:rpi0_2` - Configure a Raspberry Pi 0 2 W board 14 | 15 | This module also provides a servo model: 16 | 17 | * `viam:raspberry-pi:rpi-servo` - Configure a servo controlled by the GPIO pins on the board. Note this model is not supported on the rpi5 board. 18 | 19 | All of the Pis above are supported as [board components](https://docs.viam.com/operate/reference/components/board/), but some older models are not capable of running `viam-server`--see [Set up a computer or SBC](https://docs.viam.com/operate/get-started/setup/) for `viam-server` system requirements. 20 | 21 | ## Requirements 22 | 23 | Follow the [setup guide](https://docs.viam.com/installation/prepare/rpi-setup/) to prepare your Pi for running `viam-server` before configuring this board. 24 | 25 | Navigate to the **CONFIGURE** tab of your machine's page in the [Viam app](https://app.viam.com), searching for `raspberry-pi`. 26 | 27 | ## Configure your `raspberry-pi` board 28 | 29 | You can copy the following optional attributes to your json if you want to configure `pins`, `analogs`, and `board_settings`. These are not required to use the Raspberry Pi. 30 | 31 | ```json 32 | { 33 | "pins": [{ }], 34 | "analogs": [{ } ], 35 | "board_settings": { 36 | "enable_i2c": true, 37 | "bluetooth_enable_uart": true 38 | } 39 | } 40 | ``` 41 | 42 | ### `pins` 43 | 44 | Pins can be configured as GPIO pins and interrupts. [Interrupts](https://en.wikipedia.org/wiki/Interrupt) are a method of signaling precise state changes. Configuring digital interrupts to monitor GPIO pins on your board is useful when your application needs to know precisely when there is a change in GPIO value between high and low. 45 | Example JSON Configuration: 46 | 47 | ```json 48 | { 49 | "pins": [ 50 | { 51 | "name": "your-gpio-1", 52 | "pin": "13", 53 | "type": "gpio" 54 | }, 55 | { 56 | "name": "your-gpio-2", 57 | "pin": "14", 58 | "pull": "down" 59 | }, 60 | { 61 | "name": "your-interrupt-1", 62 | "pin": "15", 63 | "type": "interrupt" 64 | }, 65 | { 66 | "name": "your-interrupt-2", 67 | "pin": "16", 68 | "type": "interrupt", 69 | "pull": "down" 70 | } 71 | ] 72 | } 73 | ``` 74 | 75 | The following attributes are available for `pins`: 76 | 77 | | Name | Type | Required? | Description | 78 | | ---- | ---- | --------- | ----------- | 79 | |`pin`| string | **Required** | The pin number of the board's GPIO pin that you wish to configure the digital interrupt for. | 80 | |`name` | string | Optional | Your name for the digital interrupt. | 81 | |`type`| string | Optional | Whether the pin should be an `interrupt` or `gpio` pin. Default: `"gpio"` | 82 | |`pull`| string | Optional | Define whether the pins should be pull up or pull down. Omitting this uses your Pi's default configuration | 83 | |`debounce_ms`| string | Optional | define a signal debounce for your interrupts to help prevent false triggers. | 84 | 85 | * When an interrupt configured on your board processes a change in the state of the GPIO pin it is configured to monitor, it ticks to record the state change. You can stream these ticks with the board API's [`StreamTicks()`](https://docs.viam.com/components/board/#streamticks), or get the current value of the digital interrupt with Value(). 86 | * Calling [`GetGPIO()`](https://docs.viam.com/components/board/#getgpio) on a GPIO pin, which you can do without configuring interrupts, is useful when you want to know a pin's value at specific points in your program, but is less precise and convenient than using an interrupt. 87 | 88 | ### `analogs` 89 | 90 | An [analog-to-digital converter](https://www.electronics-tutorials.ws/combination/analogue-to-digital-converter.html) (ADC) takes a continuous voltage input (analog signal) and converts it to an discrete integer output (digital signal). 91 | 92 | ADCs are useful when building a robot, as they enable your board to read the analog signal output by most types of [sensors](https://docs.viam.com/components/sensor/) and other hardware components. 93 | 94 | To integrate an ADC into your machine, you must first physically connect the pins on your ADC to your board. The Pi 5 board does not currently support the use of analogs. 95 | 96 | Then, integrate `analogs` into the `attributes` of your board by adding the following to your board's JSON configuration: 97 | 98 | ```json 99 | "analogs": [ 100 | { 101 | "name": "current", 102 | "channel": "1", 103 | "spi_bus": "1", 104 | "chip_select": "0", 105 | "average_over_ms": 10, 106 | "samples_per_sec": 1 107 | }, 108 | { 109 | "name": "pressure", 110 | "channel": "0", 111 | "spi_bus": "1", 112 | "chip_select": "0" 113 | } 114 | ] 115 | ``` 116 | 117 | The following attributes are available for `analogs`: 118 | 119 | | Name | Type | Required? | Description | 120 | | ---- | ---- | --------- | ----------- | 121 | | `name` | string | **Required** | Your name for the analog reader. | 122 | | `channel` | string | **Required** | The pin number of the ADC's connection pin, wired to the board. This should be labeled as the physical index of the pin on the ADC. | 123 | | `chip_select` | string | **Required** | The chip select index of the board's connection pin, wired to the ADC. | 124 | | `spi_bus` | string | **Required** | The index of the SPI bus connecting the ADC and board. | 125 | | `average_over_ms` | int | Optional | Duration in milliseconds over which the rolling average of the analog input should be taken. | 126 | | `samples_per_sec` | int | Optional | Sampling rate of the analog input in samples per second. | 127 | 128 | ### `board_settings` 129 | 130 | The `board_settings` section allows you to configure board-level settings. 131 | 132 | #### `enable_i2c` 133 | 134 | The I2C interface on Raspberry Pi is disabled by default. When you set `enable_i2c` to `true`, the module will automatically configure your Raspberry Pi to enable I2C communication. 135 | 136 | ```json 137 | { 138 | "board_settings": { 139 | "enable_i2c": true 140 | } 141 | } 142 | ``` 143 | 144 | When I2C is enabled, the module will: 145 | 146 | 1. Add `dtparam=i2c_arm=on` to `/boot/config.txt` (or `/boot/firmware/config.txt` on newer systems). 147 | 2. Add `i2c-dev` to `/etc/modules` to ensure the I2C device interface is available. 148 | 3. Log the configuration changes for your reference. 149 | 4. **Automatically reboot the system** if changes were made. 150 | 151 | **Important Notes:** 152 | 153 | * The system will automatically reboot when I2C configuration changes are made. 154 | * If I2C is already enabled, no reboot will occur. 155 | * Setting this value to false will not disable I2C. 156 | 157 | The following attributes are available for I2C configuration: 158 | 159 | | Name | Type | Required? | Description | 160 | | ---- | ---- | --------- | ----------- | 161 | | `board_settings` | object | Optional | Board-level configuration settings | 162 | | `board_settings.enable_i2c` | boolean | Optional | Enable I2C interface on the Raspberry Pi. Default: `false` | 163 | 164 | #### `bluetooth settings` 165 | 166 | There are several generations of Bluetooth chipsets / firmware in the Raspberry Pi models. These `bluetooth_*` parameters can be used to control config.txt settings related to Bluetooth enablement and speeds. Various combinations of these bluetooth settings can, for example, enable Bluetooth tethering. 167 | 168 | ```json 169 | { 170 | "board_settings": { 171 | "bluetooth_enable_uart": true, 172 | "bluetooth_baud_rate": 576000, 173 | "bluetooth_dtoverlay_miniuart": false 174 | } 175 | } 176 | ``` 177 | 178 | If there are bluetooth related parameters pre-existing in the config.txt, the absence of Viam `board_settings` parameters will **not** change them. The bluetooth related parameters in config.txt will only be modified / enforced if the `bluetooth_*` settings are present here. 179 | 180 | When `bluetooth_enable_uart` key/value is `true`, the config.txt parameter will be set to `enable_uart=1`. When `bluetooth_enable_uart` key/value is `false`, the config.txt parameter will be set to `enable_uart=0`. Note that on a Raspberry Pi4, the default behavior, when absent, is `enable_uart=1`. You can override that behavior with `bluetooth_enable_uart:false`, which will set `enable_uart=0` 181 | 182 | When `bluetooth_dtoverlay_miniuart` key/value is `true`, the config.txt parameter will be set to `dtoverlay=miniuart-bt`. When `bluetooth_dtoverlay_miniuart` key/value is `false`, the `dtoverlay=miniuart-bt` parameter will removed from config.txt 183 | 184 | The `bluetooth_baud_rate` parameter can be used to set the `dtparam=krnbt_baudrate=` in config.txt. If you want to remove any previous `dtparam=krnbt_baudrate=` from config.txt, set `bluetooth_baud_rate: 0` 185 | 186 | * Examples of baud_rates: 187 | 188 | ```text 189 | dtparam=krnbt_baudrate=921600 190 | dtparam=krnbt_baudrate=576000 191 | dtparam=krnbt_baudrate=460800 192 | dtparam=krnbt_baudrate=230400 193 | dtparam=krnbt_baudrate=115200 194 | ``` 195 | 196 | **Important Notes:** 197 | 198 | * The system will automatically reboot when Bluetooth configuration changes are made. 199 | 200 | The following attributes are available for Bluetooth configuration: 201 | 202 | | Name | Type | Required? | Description | 203 | | ---- | ---- | --------- | ----------- | 204 | | `board_settings` | object | Optional | Board-level configuration settings | 205 | | `board_settings.bluetooth_enable_uart` | boolean | Optional | Enable/Disable the Bluetooth enable_uart on the Raspberry Pi. Default: system settings | 206 | | `board_settings.bluetooth_dtoverlay_miniuart` | boolean | Optional | the `dtoverlay=miniuart-bt` will enabled the serial uart, at a lower, but stable rate. | 207 | | `board_settings.bluetooth_baud_rate` | int | Optional | Control the baud speed (eg 921600, 576000, 460800, 230400) | 208 | 209 | ## Configure your pi servo 210 | 211 | Navigate to the **CONFIGURE** tab of your machine's page in the [Viam app](https://app.viam.com), searching for `rpi-servo` 212 | 213 | Fill in the attributes as applicable to your servo, according to the example below. 214 | 215 | ```json 216 | { 217 | "pin": "11", 218 | "board": "board-`" 219 | } 220 | ``` 221 | 222 | ### Servo attributes 223 | 224 | The following attributes are available for `viam:raspberry-pi:rpi-servo` servos: 225 | 226 | | Name | Type | Required? | Description | 227 | | ---- | ---- | --------- | ----------- | 228 | | `pin` | string | **Required** | The pin number of the pin the servo's control wire is wired to on the board. | 229 | | `board` | string | **Required** | `name` of the board the servo is wired to. | 230 | | `min` | float | Optional | Sets a software limit on the minimum angle in degrees your servo can rotate to.
Default = `0.0`
Range = [`0.0`, `180.0`] | 231 | | `max` | float | Optional | Sets a software limit on the maximum angle in degrees your servo can rotate to.
Default = `180.0`
Range = [`0.0`, `180.0`] | 232 | | `starting_position_degs` | float | Optional | Starting position of the servo in degrees.
Default = `0.0`
Range = [`0.0`, `180.0`] | 233 | | `hold_position` | boolean | Optional | If `false`, power down a servo if it has tried and failed to go to a position for a duration of 500 milliseconds.
Default = `true` | 234 | | `max_rotation_deg` | int | Optional | The maximum angle that you know your servo can possibly rotate to, according to its hardware. Refer to your servo's data sheet for clarification. Must be greater than or equal to the value you set for `max`.
Default = `180` | 235 | | `frequency_hz` | int | Optional | Servo refresh rate control value. Use this value to control the servo at more granular frequencies. Refer to your servo's data sheet for optimal operating frequency and operating rotation range. Default: `50` | 236 | 237 | ## Local development 238 | 239 | ### Building 240 | 241 | Module needs to be built from within `canon`. As of August 2024 this module is being built only in `bullseye` and supports `bullseye` and `bookworm` versions of Debian. 242 | `make module` will create raspberry-pi-module.tar.gz. 243 | 244 | ```bash 245 | canon 246 | make module 247 | ``` 248 | 249 | Then copy the tar.gz over to your pi 250 | 251 | ```bash 252 | scp /path-to/raspberry-pi-module.tar.gz your_rpi@pi.local:~ 253 | ``` 254 | 255 | Now you can use it as a [local module](https://docs.viam.com/how-tos/create-module/#test-your-module-locally)! 256 | 257 | ### Linting 258 | 259 | Linting also needs to be done from within `canon` 260 | 261 | ```bash 262 | canon 263 | make lint 264 | ``` 265 | 266 | ### Testing 267 | 268 | > [!NOTE] 269 | >All tests require a functioning raspberry pi4! 270 | 271 | Run the following in a pi 272 | 273 | ```bash 274 | make test 275 | ``` 276 | 277 | This will create binaries for each test file in /bin and run them. 278 | 279 | ## For Devs 280 | 281 | ### Module Structure 282 | 283 | The directory structure is as follows: 284 | 285 | * `rpi`: Contains all files necessary to define `viam:raspberry-pi:rpi`. Files are organized by functionality. 286 | * `rpi-servo`: Contains all files necessary to define `viam:raspberry-pi:rpi-servo`. Files are organized by functionality 287 | * `utils`: Any utility functions that are either universal to the boards or shared between `rpi` and `rpi-servo`. Included are daemon errors, pin mappings, and digital interrupts 288 | * `testing`: External package exports. Tests the components how an outside package would use the components (w/o any internal functions). 289 | 290 | ### pigpiod 291 | 292 | The module relies on the pigpio daemon to carry out GPIO functionality. The daemon accepts socket and pipe connections over the local network. Although many things can be configured, from DMA allocation mode to socket port to sample rate, we use the default settings, which match with the traditional pigpio library's defaults. More info can be seen here: . 293 | 294 | The daemon essentially supports all the same functionality as the traditional library. Instead of using pigpio.h C library, it uses the daemon library, which is mostly identical: pigpiod_if2.h. Details can be found here: 295 | 296 | ### Next steps 297 | 298 | To test your board or servo, click on the [**Test** panel](https://docs.viam.com/fleet/control) on your component's configuration page or on the **CONTROL** page. 299 | 300 | If you want to see how you can make an LED blink with your Raspberry Pi, see [Make an LED Blink With Buttons And With Code](https://docs.viam.com/tutorials/get-started/blink-an-led/). 301 | -------------------------------------------------------------------------------- /rpi/board.go: -------------------------------------------------------------------------------- 1 | // Package rpi implements raspberry pi board 2 | package rpi 3 | 4 | /* 5 | This driver contains various functionalities of raspberry pi board using the 6 | pigpio daemon library (https://abyz.me.uk/rpi/pigpio/pdif2.html). 7 | NOTE: This driver only supports software PWM functionality of raspberry pi. 8 | For software PWM, we currently support the default sample rate of 9 | 5 microseconds, which supports the following 18 frequencies (Hz): 10 | 8000 4000 2000 1600 1000 800 500 400 320 11 | 250 200 160 100 80 50 40 20 10 12 | Details on this can be found here -> https://abyz.me.uk/rpi/pigpio/pdif2.html#set_PWM_frequency 13 | */ 14 | 15 | // #include 16 | // #include 17 | // #include "pi.h" 18 | // #cgo LDFLAGS: -lpigpiod_if2 19 | import "C" 20 | 21 | import ( 22 | "context" 23 | "errors" 24 | "fmt" 25 | "os" 26 | "strconv" 27 | "strings" 28 | "sync" 29 | "time" 30 | 31 | rpiutils "raspberry-pi/utils" 32 | 33 | "go.uber.org/multierr" 34 | pb "go.viam.com/api/component/board/v1" 35 | "go.viam.com/rdk/components/board" 36 | "go.viam.com/rdk/components/board/pinwrappers" 37 | "go.viam.com/rdk/grpc" 38 | "go.viam.com/rdk/logging" 39 | "go.viam.com/rdk/resource" 40 | "go.viam.com/utils" 41 | ) 42 | 43 | // Model represents a raspberry pi board model. 44 | var ( 45 | ModelPi = rpiutils.RaspiFamily.WithModel("rpi") // Raspberry Pi Generic model 46 | ModelPi4 = rpiutils.RaspiFamily.WithModel("rpi4") // Raspberry Pi 4 model 47 | ModelPi3 = rpiutils.RaspiFamily.WithModel("rpi3") // Raspberry Pi 3 model 48 | ModelPi2 = rpiutils.RaspiFamily.WithModel("rpi2") // Raspberry Pi 2 model 49 | ModelPi1 = rpiutils.RaspiFamily.WithModel("rpi1") // Raspberry Pi 1 model 50 | ModelPi0_2 = rpiutils.RaspiFamily.WithModel("rpi0_2") // Raspberry Pi 0_2 model 51 | ModelPi0 = rpiutils.RaspiFamily.WithModel("rpi0") // Raspberry Pi 0 model 52 | ) 53 | 54 | var ( 55 | boardInstance *piPigpio // global instance of raspberry pi borad for interrupt callbacks 56 | boardInstanceMu sync.RWMutex // mutex to protect boardInstance 57 | ) 58 | 59 | // init registers a pi board based on pigpio. 60 | func init() { 61 | resource.RegisterComponent( 62 | board.API, 63 | ModelPi, 64 | resource.Registration[board.Board, *rpiutils.Config]{ 65 | Constructor: newPigpio, 66 | }) 67 | resource.RegisterComponent( 68 | board.API, 69 | ModelPi4, 70 | resource.Registration[board.Board, *rpiutils.Config]{ 71 | Constructor: newPigpio, 72 | }) 73 | resource.RegisterComponent( 74 | board.API, 75 | ModelPi3, 76 | resource.Registration[board.Board, *rpiutils.Config]{ 77 | Constructor: newPigpio, 78 | }) 79 | resource.RegisterComponent( 80 | board.API, 81 | ModelPi2, 82 | resource.Registration[board.Board, *rpiutils.Config]{ 83 | Constructor: newPigpio, 84 | }) 85 | resource.RegisterComponent( 86 | board.API, 87 | ModelPi1, 88 | resource.Registration[board.Board, *rpiutils.Config]{ 89 | Constructor: newPigpio, 90 | }) 91 | resource.RegisterComponent( 92 | board.API, 93 | ModelPi0_2, 94 | resource.Registration[board.Board, *rpiutils.Config]{ 95 | Constructor: newPigpio, 96 | }) 97 | resource.RegisterComponent( 98 | board.API, 99 | ModelPi0, 100 | resource.Registration[board.Board, *rpiutils.Config]{ 101 | Constructor: newPigpio, 102 | }) 103 | } 104 | 105 | // piPigpio is an implementation of a board.Board of a Raspberry Pi 106 | // accessed via pigpio. 107 | type piPigpio struct { 108 | resource.Named 109 | model string 110 | 111 | mu sync.Mutex 112 | cancelCtx context.Context 113 | cancelFunc context.CancelFunc 114 | pinConfigs []rpiutils.PinConfig 115 | gpioPins map[int]*rpiGPIO 116 | analogReaders map[string]*pinwrappers.AnalogSmoother 117 | // `interrupts` maps interrupt names to the interrupts. `interruptsHW` maps broadcom addresses 118 | // to these same values. The two should always have the same set of values. 119 | interrupts map[uint]*rpiInterrupt 120 | logger logging.Logger 121 | isClosed bool 122 | 123 | piID C.int // id to communicate with pigpio daemon 124 | 125 | pulls map[int]string // mapping of gpio pin to pull up/down 126 | 127 | activeBackgroundWorkers sync.WaitGroup 128 | } 129 | 130 | var ( 131 | pigpioInitialized bool 132 | // To prevent deadlocks, we must never lock the mutex of a specific piPigpio struct, above, 133 | // while this is locked. It is okay to lock this while one of those other mutexes is locked 134 | // instead. 135 | instanceMu sync.RWMutex 136 | instances = map[*piPigpio]struct{}{} 137 | ) 138 | 139 | // newPigpio makes a new pigpio based Board using the given config. 140 | func newPigpio( 141 | ctx context.Context, 142 | _ resource.Dependencies, 143 | conf resource.Config, 144 | logger logging.Logger, 145 | ) (board.Board, error) { 146 | piModel, err := os.ReadFile("/proc/device-tree/model") 147 | if err != nil { 148 | logger.Errorw("Cannot determine raspberry pi model", "error", err) 149 | } 150 | isPi5 := strings.Contains(string(piModel), "Raspberry Pi 5") 151 | if isPi5 { 152 | return nil, rpiutils.WrongModelErr(conf.Name) 153 | } 154 | 155 | piID, err := initializePigpio() 156 | if err != nil { 157 | return nil, err 158 | } 159 | logger.CInfo(ctx, "successfully started pigpiod") 160 | 161 | cancelCtx, cancelFunc := context.WithCancel(context.Background()) 162 | piInstance := &piPigpio{ 163 | Named: conf.ResourceName().AsNamed(), 164 | logger: logger, 165 | isClosed: false, 166 | cancelCtx: cancelCtx, 167 | cancelFunc: cancelFunc, 168 | piID: piID, 169 | model: conf.Model.Name, 170 | interrupts: make(map[uint]*rpiInterrupt), 171 | } 172 | 173 | if err := piInstance.Reconfigure(ctx, nil, conf); err != nil { 174 | // This has to happen outside of the lock to avoid a deadlock with interrupts. 175 | C.pigpio_stop(piID) 176 | logger.CError(ctx, "Pi GPIO terminated due to failed init.") 177 | return nil, err 178 | } 179 | 180 | return piInstance, nil 181 | } 182 | 183 | // Function initializes connection to pigpio daemon. 184 | func initializePigpio() (C.int, error) { 185 | boardInstanceMu.Lock() 186 | defer boardInstanceMu.Unlock() 187 | 188 | piID := C.pigpio_start(nil, nil) 189 | if int(piID) < 0 { 190 | // failed to init, check for common causes 191 | _, err := os.Stat("/sys/bus/platform/drivers/raspberrypi-firmware") 192 | if err != nil { 193 | return -1, errors.New("not running on a pi") 194 | } 195 | if os.Getuid() != 0 { 196 | return -1, errors.New("not running as root, try sudo") 197 | } 198 | return -1, rpiutils.ConvertErrorCodeToMessage(int(piID), "error") 199 | } 200 | 201 | return piID, nil 202 | } 203 | 204 | func (pi *piPigpio) Reconfigure( 205 | ctx context.Context, 206 | _ resource.Dependencies, 207 | conf resource.Config, 208 | ) error { 209 | cfg, err := resource.NativeConfig[*rpiutils.Config](conf) 210 | if err != nil { 211 | return err 212 | } 213 | // make sure every pin has a name. We already know every pin has a pin 214 | for _, c := range cfg.Pins { 215 | if c.Name == "" { 216 | c.Name = c.Pin 217 | } 218 | } 219 | 220 | pi.mu.Lock() 221 | defer pi.mu.Unlock() 222 | 223 | if err := pi.reconfigureAnalogReaders(cfg); err != nil { 224 | return err 225 | } 226 | 227 | if err := pi.reconfigureGPIOs(cfg); err != nil { 228 | return err 229 | } 230 | 231 | // This is the only one that actually uses ctx, but we pass it to all previous helpers, too, to 232 | // keep the interface consistent. 233 | if err := pi.reconfigureInterrupts(cfg); err != nil { 234 | return err 235 | } 236 | 237 | if err := pi.reconfigurePulls(cfg); err != nil { 238 | return err 239 | } 240 | 241 | if err := pi.configureI2C(cfg); err != nil { 242 | return err 243 | } 244 | 245 | if err := pi.configureBT(cfg); err != nil { 246 | return err 247 | } 248 | 249 | pi.pinConfigs = cfg.Pins 250 | 251 | boardInstanceMu.Lock() 252 | defer boardInstanceMu.Unlock() 253 | boardInstance = pi 254 | 255 | return nil 256 | } 257 | 258 | func (pi *piPigpio) reconfigurePulls(cfg *rpiutils.Config) error { 259 | for _, pullConf := range cfg.Pins { 260 | // skip pins that do not have a pull state set 261 | if pullConf.PullState == rpiutils.PullDefault { 262 | continue 263 | } 264 | gpioNum, have := rpiutils.BroadcomPinFromHardwareLabel(pullConf.Pin) 265 | if !have { 266 | return fmt.Errorf("error configuring pull: no gpio pin found for %s", pullConf.Name) 267 | } 268 | switch pullConf.PullState { 269 | case rpiutils.PullNone: 270 | if result := C.setPullNone(pi.piID, C.int(gpioNum)); result != 0 { 271 | pi.logger.Error(rpiutils.ConvertErrorCodeToMessage(int(result), "error")) 272 | } 273 | case rpiutils.PullUp: 274 | if result := C.setPullUp(pi.piID, C.int(gpioNum)); result != 0 { 275 | pi.logger.Error(rpiutils.ConvertErrorCodeToMessage(int(result), "error")) 276 | } 277 | case rpiutils.PullDown: 278 | if result := C.setPullDown(pi.piID, C.int(gpioNum)); result != 0 { 279 | pi.logger.Error(rpiutils.ConvertErrorCodeToMessage(int(result), "error")) 280 | } 281 | default: 282 | return fmt.Errorf("error configuring gpio pin %v pull: unexpected pull method %v", pullConf.Name, pullConf.PullState) 283 | } 284 | 285 | } 286 | return nil 287 | } 288 | 289 | func (pi *piPigpio) configureBT(cfg *rpiutils.Config) error { 290 | var configChanged bool = false 291 | var configFailed bool = false 292 | var err error 293 | configPath := rpiutils.GetBootConfigPath() 294 | 295 | // Handle enable_uart 296 | if cfg.BoardSettings.BTenableuart != nil { 297 | pi.logger.Debugf("cfg.BoardSettings.BTenableuart=%v", *cfg.BoardSettings.BTenableuart) 298 | 299 | if *cfg.BoardSettings.BTenableuart == true { 300 | // remove any previous enable_uart=0 settings 301 | configChanged, err = rpiutils.RemoveConfigParam(configPath, "enable_uart=0", pi.logger) 302 | if err != nil { 303 | pi.logger.Errorf("Failed to modify Bluetooth settings in boot config: %v", err) 304 | configFailed = true 305 | } 306 | pi.logger.Infof("Setting enable_uart=1 in config.txt") 307 | configChanged, err = rpiutils.UpdateConfigFile(configPath, "enable_uart", "=1", pi.logger) 308 | if err != nil { 309 | pi.logger.Errorf("Failed to modify Bluetooth settings in boot config: %v", err) 310 | configFailed = true 311 | } 312 | } else if *cfg.BoardSettings.BTenableuart == false { 313 | // remove any previous enable_uart=1 settings 314 | configChanged, err = rpiutils.RemoveConfigParam(configPath, "enable_uart=1", pi.logger) 315 | if err != nil { 316 | pi.logger.Errorf("Failed to modify Bluetooth settings in boot config: %v", err) 317 | configFailed = true 318 | } 319 | pi.logger.Infof("Setting enable_uart=0 in config.txt") 320 | configChanged, err = rpiutils.UpdateConfigFile(configPath, "enable_uart", "=0", pi.logger) 321 | if err != nil { 322 | pi.logger.Errorf("Failed to modify Bluetooth settings in boot config: %v", err) 323 | configFailed = true 324 | } 325 | } 326 | } 327 | 328 | // Handle dtoverlay=miniuart-bt 329 | if cfg.BoardSettings.BTdtoverlay != nil { 330 | pi.logger.Debugf("cfg.BoardSettings.BTdtoverlay=%v", *cfg.BoardSettings.BTdtoverlay) 331 | if *cfg.BoardSettings.BTdtoverlay == true { 332 | pi.logger.Infof("Adding dtoverlay=miniuart-bt to config.txt") 333 | configChanged, err = rpiutils.UpdateConfigFile(configPath, "dtoverlay=miniuart-bt", "", pi.logger) 334 | if err != nil { 335 | pi.logger.Errorf("Failed to modify Bluetooth settings in boot config: %v", err) 336 | configFailed = true 337 | } 338 | } else if *cfg.BoardSettings.BTdtoverlay == false { 339 | // remove any "dtoverylay=miniuart-bt" 340 | pi.logger.Infof("Remove dtoverlay=miniuart-bt from config.txt if it exists") 341 | configChanged, err = rpiutils.RemoveConfigParam(configPath, "dtoverlay=miniuart-bt", pi.logger) 342 | if err != nil { 343 | pi.logger.Errorf("Failed to modify Bluetooth settings in boot config: %v", err) 344 | configFailed = true 345 | } 346 | } 347 | } 348 | 349 | // Handle dtparam=krnbt_baudrate 350 | if cfg.BoardSettings.BTkbaudrate != nil { 351 | pi.logger.Debugf("cfg.BoardSettings.BTkbaudrate=%v", *cfg.BoardSettings.BTkbaudrate) 352 | 353 | // Always remove any previous dtparam=krnbt_baudrate setting before adding a potentially different value. 354 | if !configFailed { 355 | pi.logger.Debugf("Remove any line that starts with dtparam=krnbt_baudrate") 356 | configChanged, err = rpiutils.RemoveConfigParam(configPath, "dtparam=krnbt_baudrate", pi.logger) 357 | if err != nil { 358 | pi.logger.Errorf("Failed to modify Bluetooth settings in boot config: %v", err) 359 | configFailed = true 360 | } 361 | } 362 | 363 | // Add dtparam=krnbt_baudrate= 364 | // if cfg.BoardSettings.BTkbaudrate is 0 on a Raspberry Pi5, the chipset/firmware will operate at full speed 365 | // cfg.BoardSettings.BTkbaudrate == 0 is how to remove the param from config.txt 366 | if *cfg.BoardSettings.BTkbaudrate != 0 { 367 | pi.logger.Infof("Adding dtparam=krnbt_baudrate=%v in config.txt", *cfg.BoardSettings.BTkbaudrate) 368 | configChanged, err = rpiutils.UpdateConfigFile(configPath, "dtparam=krnbt_baudrate", "="+strconv.Itoa(*cfg.BoardSettings.BTkbaudrate), pi.logger) 369 | if err != nil { 370 | pi.logger.Errorf("Failed to modify Bluetooth settings in boot config: %v", err) 371 | configFailed = true 372 | } 373 | } 374 | } 375 | 376 | if configFailed { 377 | pi.logger.Errorf("Automatic Bluetooth configuration failed. Please manually edit config.txt") 378 | return nil 379 | } 380 | 381 | if configChanged { 382 | pi.logger.Infof("Bluetooth configuration modified. Initiating automatic reboot...") 383 | go rpiutils.PerformReboot(pi.logger) 384 | } 385 | 386 | return nil 387 | } 388 | 389 | func (pi *piPigpio) configureI2C(cfg *rpiutils.Config) error { 390 | pi.logger.Debugf("cfg.BoardSettings.TurnI2COn=%v", cfg.BoardSettings.TurnI2COn) 391 | // Only enable I2C if turn_i2c_on is true, otherwise do nothing 392 | if !cfg.BoardSettings.TurnI2COn { 393 | return nil 394 | } 395 | 396 | var configChanged, moduleChanged bool 397 | var err error 398 | var configFailed, moduleFailed bool 399 | 400 | configChanged, err = pi.updateI2CConfig("=on") 401 | if err != nil { 402 | pi.logger.Errorf("Failed to enable I2C in boot config: %v", err) 403 | configFailed = true 404 | } 405 | 406 | moduleChanged, err = pi.updateI2CModule(true) 407 | if err != nil { 408 | pi.logger.Errorf("Failed to enable I2C module: %v", err) 409 | moduleFailed = true 410 | } 411 | 412 | if configFailed || moduleFailed { 413 | pi.logger.Errorf("Automatic I2C configuration failed. Please manually enable I2C using 'sudo raspi-config' -> Interfacing Options -> I2C") 414 | return nil 415 | } 416 | 417 | if configChanged || moduleChanged { 418 | pi.logger.Infof("I2C configuration enabled. Initiating automatic reboot...") 419 | go rpiutils.PerformReboot(pi.logger) 420 | } 421 | 422 | return nil 423 | } 424 | 425 | func (pi *piPigpio) updateI2CConfig(desiredValue string) (bool, error) { 426 | configPath := rpiutils.GetBootConfigPath() 427 | return rpiutils.UpdateConfigFile(configPath, "dtparam=i2c_arm", desiredValue, pi.logger) 428 | } 429 | 430 | func (pi *piPigpio) updateI2CModule(enable bool) (bool, error) { 431 | return rpiutils.UpdateModuleFile("/etc/modules", "i2c-dev", enable, pi.logger) 432 | } 433 | 434 | // Close attempts to close all parts of the board cleanly. 435 | func (pi *piPigpio) Close(ctx context.Context) error { 436 | pi.mu.Lock() 437 | defer pi.mu.Unlock() 438 | 439 | if pi.isClosed { 440 | pi.logger.Info("Duplicate call to close pi board detected, skipping") 441 | return nil 442 | } 443 | 444 | pi.cancelFunc() 445 | pi.activeBackgroundWorkers.Wait() 446 | 447 | var err error 448 | err = multierr.Combine(err, 449 | closeAnalogReaders(ctx, pi), 450 | teardownInterrupts(pi)) 451 | 452 | boardInstanceMu.Lock() 453 | boardInstance = nil 454 | boardInstanceMu.Unlock() 455 | // TODO: test this with multiple instences of the board. 456 | C.pigpio_stop(pi.piID) 457 | pi.logger.CDebug(ctx, "Pi GPIO terminated properly.") 458 | 459 | pi.isClosed = true 460 | return err 461 | } 462 | 463 | // StreamTicks starts a stream of digital interrupt ticks. 464 | func (pi *piPigpio) StreamTicks(ctx context.Context, interrupts []board.DigitalInterrupt, ch chan board.Tick, 465 | extra map[string]interface{}, 466 | ) error { 467 | for _, i := range interrupts { 468 | rpiutils.AddCallback(i.(*rpiutils.BasicDigitalInterrupt), ch) 469 | } 470 | 471 | pi.activeBackgroundWorkers.Add(1) 472 | 473 | utils.ManagedGo(func() { 474 | // Wait until it's time to shut down then remove callbacks. 475 | select { 476 | case <-ctx.Done(): 477 | case <-pi.cancelCtx.Done(): 478 | } 479 | for _, i := range interrupts { 480 | rpiutils.RemoveCallback(i.(*rpiutils.BasicDigitalInterrupt), ch) 481 | } 482 | }, pi.activeBackgroundWorkers.Done) 483 | 484 | return nil 485 | } 486 | 487 | func (pi *piPigpio) SetPowerMode(ctx context.Context, mode pb.PowerMode, duration *time.Duration) error { 488 | return grpc.UnimplementedError 489 | } 490 | 491 | // closeAnalogReaders closes all analog readers associated with the board. 492 | func closeAnalogReaders(ctx context.Context, pi *piPigpio) error { 493 | var err error 494 | for _, analog := range pi.analogReaders { 495 | err = multierr.Combine(err, analog.Close(ctx)) 496 | } 497 | pi.analogReaders = map[string]*pinwrappers.AnalogSmoother{} 498 | return err 499 | } 500 | 501 | // teardownInterrupts removes all hardware interrupts and cleans up. 502 | func teardownInterrupts(pi *piPigpio) error { 503 | var err error 504 | for _, rpiInterrupt := range pi.interrupts { 505 | if result := C.teardownInterrupt(rpiInterrupt.callbackID); result != 0 { 506 | err = multierr.Combine(err, rpiutils.ConvertErrorCodeToMessage(int(result), "error")) 507 | } 508 | } 509 | pi.interrupts = map[uint]*rpiInterrupt{} 510 | return err 511 | } 512 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module raspberry-pi 2 | 3 | go 1.25.1 4 | 5 | require ( 6 | github.com/edaniels/golinters v0.0.5-0.20220906153528-641155550742 7 | github.com/golangci/golangci-lint v1.61.0 8 | github.com/pkg/errors v0.9.1 9 | github.com/rhysd/actionlint v1.6.24 10 | github.com/viam-modules/pinctrl v0.0.0-20241213172831-bdf11253b4c3 11 | go.uber.org/multierr v1.11.0 12 | go.viam.com/api v0.1.477 13 | go.viam.com/rdk v0.96.0 14 | go.viam.com/test v1.2.4 15 | go.viam.com/utils v0.1.171 16 | ) 17 | 18 | require ( 19 | 4d63.com/gocheckcompilerdirectives v1.2.1 // indirect 20 | 4d63.com/gochecknoglobals v0.2.1 // indirect 21 | cloud.google.com/go v0.115.1 // indirect 22 | cloud.google.com/go/auth v0.9.3 // indirect 23 | cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect 24 | cloud.google.com/go/compute/metadata v0.7.0 // indirect 25 | cloud.google.com/go/iam v1.2.0 // indirect 26 | cloud.google.com/go/storage v1.43.0 // indirect 27 | codeberg.org/go-fonts/liberation v0.5.0 // indirect 28 | codeberg.org/go-latex/latex v0.1.0 // indirect 29 | codeberg.org/go-pdf/fpdf v0.10.0 // indirect 30 | git.sr.ht/~sbinet/gg v0.6.0 // indirect 31 | github.com/4meepo/tagalign v1.3.4 // indirect 32 | github.com/Abirdcfly/dupword v0.1.1 // indirect 33 | github.com/Antonboom/errname v0.1.13 // indirect 34 | github.com/Antonboom/nilnil v0.1.9 // indirect 35 | github.com/Antonboom/testifylint v1.4.3 // indirect 36 | github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect 37 | github.com/Crocmagnon/fatcontext v0.5.2 // indirect 38 | github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect 39 | github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.0 // indirect 40 | github.com/Masterminds/semver/v3 v3.3.0 // indirect 41 | github.com/OpenPeeDeeP/depguard/v2 v2.2.0 // indirect 42 | github.com/a8m/envsubst v1.4.2 // indirect 43 | github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b // indirect 44 | github.com/alecthomas/go-check-sumtype v0.1.4 // indirect 45 | github.com/alexkohler/nakedret/v2 v2.0.4 // indirect 46 | github.com/alexkohler/prealloc v1.0.0 // indirect 47 | github.com/alingse/asasalint v0.0.11 // indirect 48 | github.com/apache/arrow/go/arrow v0.0.0-20201229220542-30ce2eb5d4dc // indirect 49 | github.com/ashanbrown/forbidigo v1.6.0 // indirect 50 | github.com/ashanbrown/makezero v1.1.1 // indirect 51 | github.com/aybabtme/uniplot v0.0.0-20151203143629-039c559e5e7e // indirect 52 | github.com/benbjohnson/clock v1.3.5 // indirect 53 | github.com/beorn7/perks v1.0.1 // indirect 54 | github.com/bep/debounce v1.2.1 // indirect 55 | github.com/bkielbasa/cyclop v1.2.1 // indirect 56 | github.com/blizzy78/varnamelen v0.8.0 // indirect 57 | github.com/bluenviron/gortsplib/v4 v4.8.0 // indirect 58 | github.com/bombsimon/wsl/v4 v4.4.1 // indirect 59 | github.com/breml/bidichk v0.2.7 // indirect 60 | github.com/breml/errchkjson v0.3.6 // indirect 61 | github.com/bufbuild/protocompile v0.9.0 // indirect 62 | github.com/butuzov/ireturn v0.3.0 // indirect 63 | github.com/butuzov/mirror v1.2.0 // indirect 64 | github.com/campoy/embedmd v1.0.0 // indirect 65 | github.com/catenacyber/perfsprint v0.7.1 // indirect 66 | github.com/ccojocar/zxcvbn-go v1.0.2 // indirect 67 | github.com/cenkalti/backoff v2.2.1+incompatible // indirect 68 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 69 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 70 | github.com/charithe/durationcheck v0.0.10 // indirect 71 | github.com/chavacava/garif v0.1.0 // indirect 72 | github.com/chenzhekl/goply v0.0.0-20190930133256-258c2381defd // indirect 73 | github.com/chewxy/hm v1.0.0 // indirect 74 | github.com/chewxy/math32 v1.0.8 // indirect 75 | github.com/ckaznocha/intrange v0.2.0 // indirect 76 | github.com/cloudwego/base64x v0.1.4 // indirect 77 | github.com/curioswitch/go-reassign v0.2.0 // indirect 78 | github.com/daixiang0/gci v0.13.5 // indirect 79 | github.com/davecgh/go-spew v1.1.1 // indirect 80 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect 81 | github.com/denis-tingaikin/go-header v0.5.0 // indirect 82 | github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect 83 | github.com/dgottlieb/smarty-assertions v1.2.6 // indirect 84 | github.com/disintegration/imaging v1.6.2 // indirect 85 | github.com/docker/go-units v0.5.0 // indirect 86 | github.com/edaniels/golog v0.0.0-20230215213219-28954395e8d0 // indirect 87 | github.com/edaniels/lidario v0.0.0-20220607182921-5879aa7b96dd // indirect 88 | github.com/edsrzf/mmap-go v1.1.0 // indirect 89 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 // indirect 90 | github.com/ettle/strcase v0.2.0 // indirect 91 | github.com/fatih/camelcase v1.0.0 // indirect 92 | github.com/fatih/color v1.18.0 // indirect 93 | github.com/fatih/structtag v1.2.0 // indirect 94 | github.com/felixge/httpsnoop v1.0.4 // indirect 95 | github.com/firefart/nonamedreturns v1.0.5 // indirect 96 | github.com/fogleman/gg v1.3.0 // indirect 97 | github.com/fsnotify/fsnotify v1.9.0 // indirect 98 | github.com/fullstorydev/grpcurl v1.8.6 // indirect 99 | github.com/fzipp/gocyclo v0.6.0 // indirect 100 | github.com/ghostiam/protogetter v0.3.6 // indirect 101 | github.com/go-critic/go-critic v0.11.4 // indirect 102 | github.com/go-gl/mathgl v1.0.0 // indirect 103 | github.com/go-jose/go-jose/v4 v4.1.1 // indirect 104 | github.com/go-logr/logr v1.4.3 // indirect 105 | github.com/go-logr/stdr v1.2.2 // indirect 106 | github.com/go-toolsmith/astcast v1.1.0 // indirect 107 | github.com/go-toolsmith/astcopy v1.1.0 // indirect 108 | github.com/go-toolsmith/astequal v1.2.0 // indirect 109 | github.com/go-toolsmith/astfmt v1.1.0 // indirect 110 | github.com/go-toolsmith/astp v1.1.0 // indirect 111 | github.com/go-toolsmith/strparse v1.1.0 // indirect 112 | github.com/go-toolsmith/typep v1.1.0 // indirect 113 | github.com/go-viper/mapstructure/v2 v2.3.0 // indirect 114 | github.com/go-xmlfmt/xmlfmt v1.1.2 // indirect 115 | github.com/gobwas/glob v0.2.3 // indirect 116 | github.com/goccy/go-json v0.10.2 // indirect 117 | github.com/gofrs/flock v0.12.1 // indirect 118 | github.com/gogo/protobuf v1.3.2 // indirect 119 | github.com/golang-jwt/jwt/v4 v4.5.2 // indirect 120 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect 121 | github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect 122 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect 123 | github.com/golang/protobuf v1.5.4 // indirect 124 | github.com/golang/snappy v0.0.4 // indirect 125 | github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a // indirect 126 | github.com/golangci/gofmt v0.0.0-20240816233607-d8596aa466a9 // indirect 127 | github.com/golangci/misspell v0.6.0 // indirect 128 | github.com/golangci/modinfo v0.3.4 // indirect 129 | github.com/golangci/plugin-module-register v0.1.1 // indirect 130 | github.com/golangci/revgrep v0.5.3 // indirect 131 | github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed // indirect 132 | github.com/gonuts/binary v0.2.0 // indirect 133 | github.com/google/flatbuffers v2.0.6+incompatible // indirect 134 | github.com/google/go-cmp v0.7.0 // indirect 135 | github.com/google/s2a-go v0.1.8 // indirect 136 | github.com/google/uuid v1.6.0 // indirect 137 | github.com/googleapis/enterprise-certificate-proxy v0.3.3 // indirect 138 | github.com/googleapis/gax-go/v2 v2.13.0 // indirect 139 | github.com/gordonklaus/ineffassign v0.1.0 // indirect 140 | github.com/gorilla/securecookie v1.1.2 // indirect 141 | github.com/gostaticanalysis/analysisutil v0.7.1 // indirect 142 | github.com/gostaticanalysis/comment v1.4.2 // indirect 143 | github.com/gostaticanalysis/forcetypeassert v0.1.0 // indirect 144 | github.com/gostaticanalysis/nilerr v0.1.1 // indirect 145 | github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect 146 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect 147 | github.com/hashicorp/go-version v1.7.0 // indirect 148 | github.com/hashicorp/hcl v1.0.0 // indirect 149 | github.com/hexops/gotextdiff v1.0.3 // indirect 150 | github.com/improbable-eng/grpc-web v0.15.0 // indirect 151 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 152 | github.com/jedib0t/go-pretty/v6 v6.4.6 // indirect 153 | github.com/jgautheron/goconst v1.7.1 // indirect 154 | github.com/jhump/protoreflect v1.15.6 // indirect 155 | github.com/jingyugao/rowserrcheck v1.1.1 // indirect 156 | github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af // indirect 157 | github.com/jjti/go-spancheck v0.6.2 // indirect 158 | github.com/julz/importas v0.1.0 // indirect 159 | github.com/karamaru-alpha/copyloopvar v1.1.0 // indirect 160 | github.com/kellydunn/golang-geo v0.7.0 // indirect 161 | github.com/kisielk/errcheck v1.7.0 // indirect 162 | github.com/kkHAIKE/contextcheck v1.1.5 // indirect 163 | github.com/klauspost/compress v1.18.0 // indirect 164 | github.com/kulti/thelper v0.6.3 // indirect 165 | github.com/kunwardeep/paralleltest v1.0.10 // indirect 166 | github.com/kylelemons/go-gypsy v1.0.0 // indirect 167 | github.com/kyoh86/exportloopref v0.1.11 // indirect 168 | github.com/lasiar/canonicalheader v1.1.1 // indirect 169 | github.com/ldez/gomoddirectives v0.2.4 // indirect 170 | github.com/ldez/tagliatelle v0.5.0 // indirect 171 | github.com/leonklingele/grouper v1.1.2 // indirect 172 | github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect 173 | github.com/lestrrat-go/blackmagic v1.0.2 // indirect 174 | github.com/lestrrat-go/httpcc v1.0.1 // indirect 175 | github.com/lestrrat-go/iter v1.0.2 // indirect 176 | github.com/lestrrat-go/jwx v1.2.29 // indirect 177 | github.com/lestrrat-go/option v1.0.1 // indirect 178 | github.com/lib/pq v1.10.9 // indirect 179 | github.com/lmittmann/ppm v1.0.2 // indirect 180 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 181 | github.com/lufeee/execinquery v1.2.1 // indirect 182 | github.com/macabu/inamedparam v0.1.3 // indirect 183 | github.com/magiconair/properties v1.8.6 // indirect 184 | github.com/maratori/testableexamples v1.0.0 // indirect 185 | github.com/maratori/testpackage v1.1.1 // indirect 186 | github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26 // indirect 187 | github.com/mattn/go-colorable v0.1.14 // indirect 188 | github.com/mattn/go-isatty v0.0.20 // indirect 189 | github.com/mattn/go-runewidth v0.0.16 // indirect 190 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect 191 | github.com/mgechev/revive v1.3.9 // indirect 192 | github.com/miekg/dns v1.1.53 // indirect 193 | github.com/mitchellh/go-homedir v1.1.0 // indirect 194 | github.com/mitchellh/mapstructure v1.5.0 // indirect 195 | github.com/mkch/gpio v0.0.0-20190919032813-8327cd97d95e // indirect 196 | github.com/montanaflynn/stats v0.7.1 // indirect 197 | github.com/moricho/tparallel v0.3.2 // indirect 198 | github.com/muesli/clusters v0.0.0-20200529215643-2700303c1762 // indirect 199 | github.com/muesli/kmeans v0.3.1 // indirect 200 | github.com/muhlemmer/gu v0.3.1 // indirect 201 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 202 | github.com/nakabonne/nestif v0.3.1 // indirect 203 | github.com/nishanths/exhaustive v0.12.0 // indirect 204 | github.com/nishanths/predeclared v0.2.2 // indirect 205 | github.com/nunnatsa/ginkgolinter v0.16.2 // indirect 206 | github.com/olekukonko/tablewriter v0.0.5 // indirect 207 | github.com/pelletier/go-toml v1.9.5 // indirect 208 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 209 | github.com/pion/datachannel v1.5.10 // indirect 210 | github.com/pion/dtls/v2 v2.2.12 // indirect 211 | github.com/pion/interceptor v0.1.40 // indirect 212 | github.com/pion/logging v0.2.4 // indirect 213 | github.com/pion/mdns v0.0.12 // indirect 214 | github.com/pion/randutil v0.1.0 // indirect 215 | github.com/pion/rtcp v1.2.15 // indirect 216 | github.com/pion/rtp v1.8.21 // indirect 217 | github.com/pion/sctp v1.8.39 // indirect 218 | github.com/pion/sdp/v3 v3.0.15 // indirect 219 | github.com/pion/srtp/v2 v2.0.20 // indirect 220 | github.com/pion/stun v0.6.1 // indirect 221 | github.com/pion/transport/v2 v2.2.10 // indirect 222 | github.com/pion/transport/v3 v3.0.7 // indirect 223 | github.com/pion/turn/v2 v2.1.6 // indirect 224 | github.com/pmezard/go-difflib v1.0.0 // indirect 225 | github.com/polyfloyd/go-errorlint v1.6.0 // indirect 226 | github.com/prometheus/client_golang v1.22.0 // indirect 227 | github.com/prometheus/client_model v0.6.2 // indirect 228 | github.com/prometheus/common v0.64.0 // indirect 229 | github.com/prometheus/procfs v0.15.1 // indirect 230 | github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 // indirect 231 | github.com/quasilyte/go-ruleguard/dsl v0.3.22 // indirect 232 | github.com/quasilyte/gogrep v0.5.0 // indirect 233 | github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect 234 | github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect 235 | github.com/rivo/uniseg v0.4.7 // indirect 236 | github.com/robfig/cron v1.2.0 // indirect 237 | github.com/rs/cors v1.11.1 // indirect 238 | github.com/ryancurrah/gomodguard v1.3.5 // indirect 239 | github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect 240 | github.com/samber/lo v1.51.0 // indirect 241 | github.com/sanposhiho/wastedassign/v2 v2.0.7 // indirect 242 | github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect 243 | github.com/sashamelentyev/interfacebloat v1.1.0 // indirect 244 | github.com/sashamelentyev/usestdlibvars v1.27.0 // indirect 245 | github.com/securego/gosec/v2 v2.21.2 // indirect 246 | github.com/sergi/go-diff v1.4.0 // indirect 247 | github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c // indirect 248 | github.com/sirupsen/logrus v1.9.3 // indirect 249 | github.com/sivchari/containedctx v1.0.3 // indirect 250 | github.com/sivchari/tenv v1.10.0 // indirect 251 | github.com/sonatard/noctx v0.0.2 // indirect 252 | github.com/sourcegraph/go-diff v0.7.0 // indirect 253 | github.com/spf13/afero v1.11.0 // indirect 254 | github.com/spf13/cast v1.5.0 // indirect 255 | github.com/spf13/cobra v1.8.1 // indirect 256 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 257 | github.com/spf13/pflag v1.0.5 // indirect 258 | github.com/spf13/viper v1.12.0 // indirect 259 | github.com/srikrsna/protoc-gen-gotag v0.6.2 // indirect 260 | github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect 261 | github.com/stbenjam/no-sprintf-host-port v0.1.1 // indirect 262 | github.com/stretchr/objx v0.5.2 // indirect 263 | github.com/stretchr/testify v1.10.0 // indirect 264 | github.com/subosito/gotenv v1.4.1 // indirect 265 | github.com/tdakkota/asciicheck v0.2.0 // indirect 266 | github.com/tetafro/godot v1.4.17 // indirect 267 | github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966 // indirect 268 | github.com/timonwong/loggercheck v0.9.4 // indirect 269 | github.com/tomarrell/wrapcheck/v2 v2.9.0 // indirect 270 | github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect 271 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 272 | github.com/ultraware/funlen v0.1.0 // indirect 273 | github.com/ultraware/whitespace v0.1.1 // indirect 274 | github.com/uudashr/gocognit v1.1.3 // indirect 275 | github.com/viamrobotics/ice/v2 v2.3.40 // indirect 276 | github.com/viamrobotics/webrtc/v3 v3.99.16 // indirect 277 | github.com/viamrobotics/zeroconf v1.0.12 // indirect 278 | github.com/wlynxg/anet v0.0.5 // indirect 279 | github.com/xdg-go/pbkdf2 v1.0.0 // indirect 280 | github.com/xdg-go/scram v1.1.2 // indirect 281 | github.com/xdg-go/stringprep v1.0.4 // indirect 282 | github.com/xen0n/gosmopolitan v1.2.2 // indirect 283 | github.com/xfmoulet/qoi v0.2.0 // indirect 284 | github.com/xtgo/set v1.0.0 // indirect 285 | github.com/yagipy/maintidx v1.0.0 // indirect 286 | github.com/yeya24/promlinter v0.3.0 // indirect 287 | github.com/ykadowak/zerologlint v0.1.5 // indirect 288 | github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect 289 | github.com/zhuyie/golzf v0.0.0-20161112031142-8387b0307ade // indirect 290 | github.com/zitadel/oidc/v3 v3.37.0 // indirect 291 | github.com/zitadel/schema v1.3.1 // indirect 292 | github.com/ziutek/mymysql v1.5.4 // indirect 293 | gitlab.com/bosi/decorder v0.4.2 // indirect 294 | go-hep.org/x/hep v0.32.1 // indirect 295 | go-simpler.org/musttag v0.12.2 // indirect 296 | go-simpler.org/sloglint v0.7.2 // indirect 297 | go.mongodb.org/mongo-driver v1.17.1 // indirect 298 | go.opencensus.io v0.24.0 // indirect 299 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 300 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect 301 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect 302 | go.opentelemetry.io/otel v1.37.0 // indirect 303 | go.opentelemetry.io/otel/metric v1.37.0 // indirect 304 | go.opentelemetry.io/otel/trace v1.37.0 // indirect 305 | go.uber.org/automaxprocs v1.5.3 // indirect 306 | go.uber.org/goleak v1.3.0 // indirect 307 | go.uber.org/zap v1.27.0 // indirect 308 | go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2 // indirect 309 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect 310 | golang.org/x/crypto v0.41.0 // indirect 311 | golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect 312 | golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f // indirect 313 | golang.org/x/image v0.25.0 // indirect 314 | golang.org/x/mod v0.26.0 // indirect 315 | golang.org/x/net v0.43.0 // indirect 316 | golang.org/x/oauth2 v0.30.0 // indirect 317 | golang.org/x/sync v0.16.0 // indirect 318 | golang.org/x/sys v0.35.0 // indirect 319 | golang.org/x/text v0.28.0 // indirect 320 | golang.org/x/time v0.6.0 // indirect 321 | golang.org/x/tools v0.35.0 // indirect 322 | golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect 323 | gonum.org/v1/gonum v0.16.0 // indirect 324 | gonum.org/v1/plot v0.15.2 // indirect 325 | google.golang.org/api v0.196.0 // indirect 326 | google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 // indirect 327 | google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 // indirect 328 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c // indirect 329 | google.golang.org/grpc v1.75.0 // indirect 330 | google.golang.org/protobuf v1.36.8 // indirect 331 | gopkg.in/ini.v1 v1.67.0 // indirect 332 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect 333 | gopkg.in/yaml.v2 v2.4.0 // indirect 334 | gopkg.in/yaml.v3 v3.0.1 // indirect 335 | gorgonia.org/tensor v0.9.24 // indirect 336 | gorgonia.org/vecf32 v0.9.0 // indirect 337 | gorgonia.org/vecf64 v0.9.0 // indirect 338 | honnef.co/go/tools v0.5.1 // indirect 339 | mvdan.cc/gofumpt v0.7.0 // indirect 340 | mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f // indirect 341 | nhooyr.io/websocket v1.8.7 // indirect 342 | periph.io/x/conn/v3 v3.7.0 // indirect 343 | periph.io/x/host/v3 v3.8.1-0.20230331112814-9f0d9f7d76db // indirect 344 | ) 345 | 346 | // replace github.com/viam-modules/pinctrl => ../pinctrl 347 | -------------------------------------------------------------------------------- /pi5/board.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | // Package pi5 implements a raspberry pi5 board using pinctrl 4 | package pi5 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "os" 10 | "strconv" 11 | "strings" 12 | "sync" 13 | "time" 14 | 15 | rpiutils "raspberry-pi/utils" 16 | 17 | "github.com/pkg/errors" 18 | "github.com/viam-modules/pinctrl/pinctrl" 19 | "go.uber.org/multierr" 20 | pb "go.viam.com/api/component/board/v1" 21 | "go.viam.com/rdk/components/board" 22 | gl "go.viam.com/rdk/components/board/genericlinux" 23 | "go.viam.com/rdk/grpc" 24 | "go.viam.com/rdk/logging" 25 | "go.viam.com/rdk/resource" 26 | "go.viam.com/utils" 27 | ) 28 | 29 | // Model is the model for a Raspberry Pi 5. 30 | var Model = rpiutils.RaspiFamily.WithModel("rpi5") 31 | 32 | // register values for configuring pull up/pull down in mem. 33 | const ( 34 | pullNoneMode = 0x0 35 | pullDownMode = 0x4 36 | pullUpMode = 0x8 37 | ) 38 | 39 | func init() { 40 | logger := logging.NewLogger("pi5.init") 41 | gpioMappings, err := gl.GetGPIOBoardMappings(Model.Name, boardInfoMappings, logger) 42 | var noBoardErr gl.NoBoardFoundError 43 | if errors.As(err, &noBoardErr) { 44 | logger.Debugw("Error getting raspi5 GPIO board mapping", "error", err) 45 | } 46 | 47 | resource.RegisterComponent( 48 | board.API, 49 | Model, 50 | resource.Registration[board.Board, *rpiutils.Config]{ 51 | Constructor: func( 52 | ctx context.Context, 53 | _ resource.Dependencies, 54 | conf resource.Config, 55 | logger logging.Logger, 56 | ) (board.Board, error) { 57 | return newBoard(ctx, conf, gpioMappings, logger, false) 58 | }, 59 | }) 60 | } 61 | 62 | type pinctrlpi5 struct { 63 | resource.Named 64 | mu sync.Mutex 65 | 66 | gpioMappings map[string]gl.GPIOBoardMapping 67 | logger logging.Logger 68 | 69 | gpios map[uint]*pinctrl.GPIOPin 70 | interrupts map[uint]*pinctrl.DigitalInterrupt 71 | userDefinedNames map[string]uint // user defined pin names that map to a line/boardcom 72 | pinConfigs []rpiutils.PinConfig 73 | 74 | boardPinCtrl pinctrl.Pinctrl 75 | 76 | cancelCtx context.Context 77 | cancelFunc func() 78 | activeBackgroundWorkers sync.WaitGroup 79 | 80 | pulls map[int]byte // mapping of gpio pin to pull up/down 81 | } 82 | 83 | // newBoard is the constructor for a Board. 84 | func newBoard( 85 | ctx context.Context, 86 | conf resource.Config, 87 | gpioMappings map[string]gl.GPIOBoardMapping, 88 | logger logging.Logger, 89 | testingMode bool, 90 | ) (board.Board, error) { 91 | var err error 92 | piModel, err := os.ReadFile("/proc/device-tree/model") 93 | if err != nil { 94 | logger.Errorw("Cannot determine raspberry pi model", "error", err) 95 | } 96 | isPi5 := strings.Contains(string(piModel), "Raspberry Pi 5") 97 | // ensure that we are a pi5 when not running tests 98 | if !isPi5 && !testingMode { 99 | return nil, rpiutils.WrongModelErr(conf.Name) 100 | } 101 | cancelCtx, cancelFunc := context.WithCancel(context.Background()) 102 | 103 | b := &pinctrlpi5{ 104 | Named: conf.ResourceName().AsNamed(), 105 | 106 | gpioMappings: gpioMappings, 107 | logger: logger, 108 | cancelCtx: cancelCtx, 109 | cancelFunc: cancelFunc, 110 | 111 | gpios: map[uint]*pinctrl.GPIOPin{}, 112 | interrupts: map[uint]*pinctrl.DigitalInterrupt{}, 113 | 114 | pulls: map[int]byte{}, 115 | } 116 | 117 | pinctrlCfg := pinctrl.Config{ 118 | GPIOChipPath: "gpio0", DevMemPath: "/dev/gpiomem0", 119 | ChipSize: 0x30000, UseAlias: true, UseGPIOMem: true, 120 | } 121 | if testingMode { 122 | pinctrlCfg.TestPath = "./pi5/mock-device-tree" 123 | } 124 | 125 | // Note that this must be called before configuring the pull up/down configuration uses the 126 | // memory mapped in this function. 127 | b.boardPinCtrl, err = pinctrl.SetupPinControl(pinctrlCfg, logger) 128 | if err != nil { 129 | return nil, err 130 | } 131 | 132 | // Initialize the GPIO pins 133 | for newName, mapping := range gpioMappings { 134 | bcom, _ := rpiutils.BroadcomPinFromHardwareLabel(newName) 135 | b.gpios[bcom] = b.boardPinCtrl.CreateGpioPin(mapping, rpiutils.DefaultPWMFreqHz) 136 | } 137 | 138 | if err := b.Reconfigure(ctx, nil, conf); err != nil { 139 | return nil, err 140 | } 141 | 142 | return b, nil 143 | } 144 | 145 | func (b *pinctrlpi5) Reconfigure( 146 | ctx context.Context, 147 | deps resource.Dependencies, 148 | conf resource.Config, 149 | ) error { 150 | newConf, err := resource.NativeConfig[*rpiutils.Config](conf) 151 | if err != nil { 152 | return err 153 | } 154 | 155 | b.mu.Lock() 156 | defer b.mu.Unlock() 157 | 158 | // make sure every pin has a name. We already know every pin has a pin 159 | // possibly clean this up at a later date 160 | for _, c := range newConf.Pins { 161 | if c.Name == "" { 162 | c.Name = c.Pin 163 | } 164 | } 165 | 166 | if err := b.validatePins(newConf); err != nil { 167 | return err 168 | } 169 | 170 | if err := b.reconfigurePullUpPullDowns(newConf); err != nil { 171 | return err 172 | } 173 | if err := b.reconfigureInterrupts(newConf); err != nil { 174 | return err 175 | } 176 | 177 | b.configureI2C(newConf) 178 | 179 | if err := b.configureBT(newConf); err != nil { 180 | return err 181 | } 182 | 183 | b.pinConfigs = newConf.Pins 184 | 185 | return nil 186 | } 187 | 188 | // reconfigureInterrupts reconfigures the digital interrupts based on the new configuration provided. 189 | // It reuses existing interrupts when possible and creates new ones if necessary. 190 | func (b *pinctrlpi5) reconfigureInterrupts(newConf *rpiutils.Config) error { 191 | // look at previous interrupt config, and see if we removed any 192 | for _, oldConfig := range b.pinConfigs { 193 | if oldConfig.Type != rpiutils.PinInterrupt { 194 | continue 195 | } 196 | sameInterrupt := false 197 | for _, newConfig := range newConf.Pins { 198 | if newConfig.Type != rpiutils.PinInterrupt { 199 | continue 200 | } 201 | // check if we still have this interrupt 202 | if oldConfig.Name == newConfig.Name && oldConfig.Pin == newConfig.Pin { 203 | sameInterrupt = true 204 | break 205 | } 206 | } 207 | // if we still have the interrupt, don't modify it 208 | if sameInterrupt { 209 | continue 210 | } 211 | // we no longer want this interrupt, so we will remove it and add back the pin's gpio functionality 212 | bcom, ok := rpiutils.BroadcomPinFromHardwareLabel(oldConfig.Pin) 213 | if !ok { 214 | return errors.Errorf("cannot find GPIO for unknown pin: %s", oldConfig.Name) 215 | } 216 | // this actually removes the interrupt 217 | interrupt, ok := b.interrupts[bcom] 218 | if ok { 219 | if err := interrupt.Close(); err != nil { 220 | return err 221 | } 222 | delete(b.interrupts, bcom) 223 | } 224 | 225 | // add back the gpio pin to make it available to the user 226 | b.gpios[bcom] = b.boardPinCtrl.CreateGpioPin(b.gpioMappings[oldConfig.Pin], rpiutils.DefaultPWMFreqHz) 227 | } 228 | // add any new interrupts. DigitalInterruptByName will create the interrupt only if we are not already managing it. 229 | for _, newConfig := range newConf.Pins { 230 | if newConfig.Type != rpiutils.PinInterrupt { 231 | continue 232 | } 233 | if _, err := b.digitalInterruptByName(newConfig.Name, newConfig.DebounceMS); err != nil { 234 | return err 235 | } 236 | } 237 | 238 | return nil 239 | } 240 | 241 | // record all custom pin names that the user has defined in the config for lookup. 242 | func (b *pinctrlpi5) validatePins(newConf *rpiutils.Config) error { 243 | nameToPin := map[string]uint{} 244 | for _, pinConf := range newConf.Pins { 245 | // ensure the configured pin is a real pin 246 | pin, ok := b.gpioMappings[pinConf.Pin] 247 | if !ok { 248 | return fmt.Errorf("pin %v could not be found", pinConf.Pin) 249 | } 250 | // check if the pin name matches a name we handle by default 251 | _, alreadyDefined := rpiutils.BroadcomPinFromHardwareLabel(pinConf.Name) 252 | if alreadyDefined { 253 | continue 254 | } 255 | // add the new name to our list of names to track 256 | nameToPin[pinConf.Name] = uint(pin.GPIO) 257 | } 258 | b.userDefinedNames = nameToPin 259 | return nil 260 | } 261 | 262 | func (b *pinctrlpi5) reconfigurePullUpPullDowns(newConf *rpiutils.Config) error { 263 | for _, pullConf := range newConf.Pins { 264 | pin, ok := b.gpioMappings[pullConf.Pin] 265 | if !ok { 266 | return fmt.Errorf("pin %v could not be found", pullConf.Pin) 267 | } 268 | gpioNum := pin.GPIO 269 | switch pullConf.PullState { 270 | case rpiutils.PullDefault: // skip pins that do not have a pull state set 271 | continue 272 | case rpiutils.PullNone: 273 | b.pulls[gpioNum] = pullNoneMode 274 | case rpiutils.PullUp: 275 | b.pulls[gpioNum] = pullUpMode 276 | case rpiutils.PullDown: 277 | b.pulls[gpioNum] = pullDownMode 278 | default: 279 | return fmt.Errorf("error configuring gpio pin %v pull: unexpected pull method %v", pullConf.Name, pullConf.PullState) 280 | } 281 | } 282 | b.setPulls() 283 | 284 | return nil 285 | } 286 | 287 | // setPull is a helper function to access memory to set a pull up/pull down resisitor on a pin. 288 | func (b *pinctrlpi5) setPulls() { 289 | // offset to the pads address space in /dev/gpiomem0 290 | // all gpio pins are in bank0 291 | PadsBank0Offset := 0x00020000 292 | 293 | for pin, mode := range b.pulls { 294 | // each pad has 4 header bytes + 4 bytes of memory for each gpio pin 295 | pinOffsetBytes := 4 + 4*pin 296 | 297 | // only the 5th and 6th bits of the register are used to set pull up/down 298 | // reset the register then set the mode 299 | b.boardPinCtrl.VPage[PadsBank0Offset+pinOffsetBytes] = (b.boardPinCtrl.VPage[PadsBank0Offset+pinOffsetBytes] & 0xf3) | mode 300 | } 301 | } 302 | 303 | // AnalogByName returns the analog pin by the given name if it exists. 304 | func (b *pinctrlpi5) AnalogByName(name string) (board.Analog, error) { 305 | return nil, errors.New("analogs not supported") 306 | } 307 | 308 | // the implementation of digitalInterruptByName. The board mutex should be locked before calling this. 309 | func (b *pinctrlpi5) digitalInterruptByName(name string, debounceMilliSeconds int) (board.DigitalInterrupt, error) { 310 | // first check if the pinName is a user defined name 311 | bcom, ok := b.userDefinedNames[name] 312 | if !ok { 313 | // if the name is not a user defined name, then check if its a known pin 314 | bcom, ok = rpiutils.BroadcomPinFromHardwareLabel(name) 315 | if !ok { 316 | return nil, errors.Errorf("cannot find GPIO for unknown pin: %s", name) 317 | } 318 | } 319 | 320 | // if we are already managing the interrupt, then return the interrupt 321 | interrupt, ok := b.interrupts[bcom] 322 | if ok { 323 | return interrupt, nil 324 | } 325 | 326 | // Otherwise, the name is not something we recognize yet. If it appears to be a GPIO pin, we'll 327 | // remove its GPIO capabilities and turn it into a digital interrupt. 328 | gpio, ok := b.gpios[bcom] 329 | if !ok { 330 | return nil, fmt.Errorf("can't find GPIO (%s)", name) 331 | } 332 | if err := gpio.Close(); err != nil { 333 | return nil, err 334 | } 335 | 336 | hardwareName := "" 337 | var pinMapping gl.GPIOBoardMapping 338 | // When creating a new interrupt we need to pass in the genericlinux pin mapping. 339 | // Unfortunately with the bcom logic it ended up hard to track the generic linux pinmapping with the bcom number 340 | // to workaround this we have to run through all of the pinmappings to find which mapping is actually the requested version 341 | for newName, mapping := range b.gpioMappings { 342 | if mapping.GPIO == int(bcom) { 343 | hardwareName = newName 344 | pinMapping = mapping 345 | } 346 | } 347 | 348 | defaultInterruptConfig := board.DigitalInterruptConfig{ 349 | Name: hardwareName, 350 | Pin: hardwareName, 351 | } 352 | interrupt, err := b.boardPinCtrl.NewDigitalInterrupt(defaultInterruptConfig, pinMapping, debounceMilliSeconds, nil) 353 | if err != nil { 354 | return nil, err 355 | } 356 | 357 | delete(b.gpios, bcom) 358 | b.interrupts[bcom] = interrupt 359 | return interrupt, nil 360 | } 361 | 362 | // DigitalInterruptByName returns the interrupt by the given name if it exists. 363 | func (b *pinctrlpi5) DigitalInterruptByName(name string) (board.DigitalInterrupt, error) { 364 | b.mu.Lock() 365 | defer b.mu.Unlock() 366 | return b.digitalInterruptByName(name, 0) 367 | } 368 | 369 | // AnalogNames returns the names of all known analog pins. 370 | func (b *pinctrlpi5) AnalogNames() []string { 371 | return []string{} 372 | } 373 | 374 | // DigitalInterruptNames returns the names of all known digital interrupts. 375 | // Unimplemented because we do not have an api to communicate this over. 376 | func (b *pinctrlpi5) DigitalInterruptNames() []string { 377 | return nil 378 | } 379 | 380 | // GPIOPinByName returns a GPIOPin by name. 381 | func (b *pinctrlpi5) GPIOPinByName(pinName string) (board.GPIOPin, error) { 382 | // first check if the pinName is a user defined name 383 | bcom, ok := b.userDefinedNames[pinName] 384 | if !ok { 385 | // if the name is not a user defined name, then check if its a known pin 386 | bcom, ok = rpiutils.BroadcomPinFromHardwareLabel(pinName) 387 | if !ok { 388 | return nil, errors.Errorf("cannot find GPIO for unknown pin: %s", pinName) 389 | } 390 | } 391 | 392 | // check if the pin is being managed as a gpio 393 | if pin, ok := b.gpios[bcom]; ok { 394 | return pin, nil 395 | } 396 | 397 | // Check if pin is a digital interrupt: those can still be used as inputs. 398 | if interrupt, interruptOk := b.interrupts[bcom]; interruptOk { 399 | return interrupt, nil 400 | } 401 | 402 | return nil, errors.Errorf("cannot find GPIO for unknown pin: %s", pinName) 403 | } 404 | 405 | // SetPowerMode sets the board to the given power mode. If provided, 406 | // the board will exit the given power mode after the specified 407 | // duration. 408 | func (b *pinctrlpi5) SetPowerMode( 409 | ctx context.Context, 410 | mode pb.PowerMode, 411 | duration *time.Duration, 412 | ) error { 413 | return grpc.UnimplementedError 414 | } 415 | 416 | // StreamTicks starts a stream of digital interrupt ticks. 417 | func (b *pinctrlpi5) StreamTicks(ctx context.Context, interrupts []board.DigitalInterrupt, ch chan board.Tick, 418 | extra map[string]interface{}, 419 | ) error { 420 | var rawInterrupts []*pinctrl.DigitalInterrupt 421 | for _, i := range interrupts { 422 | raw, ok := i.(*pinctrl.DigitalInterrupt) 423 | if !ok { 424 | return errors.New("cannot stream ticks to an interrupt not associated with this board") 425 | } 426 | rawInterrupts = append(rawInterrupts, raw) 427 | } 428 | 429 | for _, i := range rawInterrupts { 430 | i.AddChannel(ch) 431 | } 432 | 433 | b.activeBackgroundWorkers.Add(1) 434 | utils.ManagedGo(func() { 435 | // Wait until it's time to shut down then remove callbacks. 436 | select { 437 | case <-ctx.Done(): 438 | case <-b.cancelCtx.Done(): 439 | } 440 | for _, i := range rawInterrupts { 441 | i.RemoveChannel(ch) 442 | } 443 | }, b.activeBackgroundWorkers.Done) 444 | 445 | return nil 446 | } 447 | 448 | func (b *pinctrlpi5) configureBT(cfg *rpiutils.Config) error { 449 | var configChanged bool = false 450 | var configFailed bool = false 451 | var err error 452 | configPath := rpiutils.GetBootConfigPath() 453 | 454 | // Handle enable_uart 455 | if cfg.BoardSettings.BTenableuart != nil { 456 | b.logger.Debugf("cfg.BoardSettings.BTenableuart=%v", *cfg.BoardSettings.BTenableuart) 457 | 458 | if *cfg.BoardSettings.BTenableuart == true { 459 | // remove any previous enable_uart=0 settings 460 | configChanged, err = rpiutils.RemoveConfigParam(configPath, "enable_uart=0", b.logger) 461 | if err != nil { 462 | b.logger.Errorf("Failed to modify Bluetooth settings in boot config: %v", err) 463 | configFailed = true 464 | } 465 | b.logger.Infof("Setting enable_uart=1 in config.txt") 466 | configChanged, err = rpiutils.UpdateConfigFile(configPath, "enable_uart", "=1", b.logger) 467 | if err != nil { 468 | b.logger.Errorf("Failed to modify Bluetooth settings in boot config: %v", err) 469 | configFailed = true 470 | } 471 | } else if *cfg.BoardSettings.BTenableuart == false { 472 | // remove any previous enable_uart=1 settings 473 | configChanged, err = rpiutils.RemoveConfigParam(configPath, "enable_uart=1", b.logger) 474 | if err != nil { 475 | b.logger.Errorf("Failed to modify Bluetooth settings in boot config: %v", err) 476 | configFailed = true 477 | } 478 | b.logger.Infof("Setting enable_uart=0 in config.txt") 479 | configChanged, err = rpiutils.UpdateConfigFile(configPath, "enable_uart", "=0", b.logger) 480 | if err != nil { 481 | b.logger.Errorf("Failed to modify Bluetooth settings in boot config: %v", err) 482 | configFailed = true 483 | } 484 | } 485 | } 486 | 487 | // Handle dtoverlay=miniuart-bt 488 | if cfg.BoardSettings.BTdtoverlay != nil { 489 | b.logger.Debugf("cfg.BoardSettings.BTdtoverlay=%v", *cfg.BoardSettings.BTdtoverlay) 490 | if *cfg.BoardSettings.BTdtoverlay == true { 491 | b.logger.Infof("Adding dtoverlay=miniuart-bt to config.txt") 492 | configChanged, err = rpiutils.UpdateConfigFile(configPath, "dtoverlay=miniuart-bt", "", b.logger) 493 | if err != nil { 494 | b.logger.Errorf("Failed to modify Bluetooth settings in boot config: %v", err) 495 | configFailed = true 496 | } 497 | } else if *cfg.BoardSettings.BTdtoverlay == false { 498 | // remove any "dtoverylay=miniuart-bt" 499 | b.logger.Infof("Remove dtoverlay=miniuart-bt from config.txt if it exists") 500 | configChanged, err = rpiutils.RemoveConfigParam(configPath, "dtoverlay=miniuart-bt", b.logger) 501 | if err != nil { 502 | b.logger.Errorf("Failed to modify Bluetooth settings in boot config: %v", err) 503 | configFailed = true 504 | } 505 | } 506 | } 507 | 508 | // Handle dtparam=krnbt_baudrate 509 | if cfg.BoardSettings.BTkbaudrate != nil { 510 | b.logger.Debugf("cfg.BoardSettings.BTkbaudrate=%v", *cfg.BoardSettings.BTkbaudrate) 511 | 512 | // Always remove any previous dtparam=krnbt_baudrate setting before adding a potentially different value. 513 | if !configFailed { 514 | b.logger.Debugf("Remove any line that starts with dtparam=krnbt_baudrate") 515 | configChanged, err = rpiutils.RemoveConfigParam(configPath, "dtparam=krnbt_baudrate", b.logger) 516 | if err != nil { 517 | b.logger.Errorf("Failed to modify Bluetooth settings in boot config: %v", err) 518 | configFailed = true 519 | } 520 | } 521 | 522 | // Add dtparam=krnbt_baudrate= 523 | // if cfg.BoardSettings.BTkbaudrate is 0 on a Raspberry Pi5, the chipset/firmware will operate at full speed 524 | // cfg.BoardSettings.BTkbaudrate == 0 is how to remove the param from config.txt 525 | if *cfg.BoardSettings.BTkbaudrate != 0 { 526 | b.logger.Infof("Adding dtparam=krnbt_baudrate=%v in config.txt", *cfg.BoardSettings.BTkbaudrate) 527 | configChanged, err = rpiutils.UpdateConfigFile(configPath, "dtparam=krnbt_baudrate", "="+strconv.Itoa(*cfg.BoardSettings.BTkbaudrate), b.logger) 528 | if err != nil { 529 | b.logger.Errorf("Failed to modify Bluetooth settings in boot config: %v", err) 530 | configFailed = true 531 | } 532 | } 533 | } 534 | 535 | if configFailed { 536 | b.logger.Errorf("Automatic Bluetooth configuration failed. Please manually edit config.txt") 537 | return nil 538 | } 539 | 540 | if configChanged { 541 | b.logger.Infof("Bluetooth configuration modified. Initiating automatic reboot...") 542 | go rpiutils.PerformReboot(b.logger) 543 | } 544 | 545 | return nil 546 | } 547 | 548 | func (b *pinctrlpi5) configureI2C(cfg *rpiutils.Config) { 549 | b.logger.Debugf("cfg.BoardSettings.TurnI2COn=%v", cfg.BoardSettings.TurnI2COn) 550 | // Only enable I2C if turn_i2c_on is true, otherwise do nothing 551 | if !cfg.BoardSettings.TurnI2COn { 552 | return 553 | } 554 | 555 | var configChanged, moduleChanged bool 556 | var err error 557 | var configFailed, moduleFailed bool 558 | 559 | configChanged, err = b.updateI2CConfig("=on") 560 | if err != nil { 561 | b.logger.Errorf("Failed to enable I2C in boot config: %v", err) 562 | configFailed = true 563 | } 564 | 565 | moduleChanged, err = b.updateI2CModule(true) 566 | if err != nil { 567 | b.logger.Errorf("Failed to enable I2C module: %v", err) 568 | moduleFailed = true 569 | } 570 | 571 | if configFailed || moduleFailed { 572 | b.logger.Errorf("Automatic I2C configuration failed. Please manually enable I2C using 'sudo raspi-config' -> Interfacing Options -> I2C") 573 | return 574 | } 575 | 576 | if configChanged || moduleChanged { 577 | b.logger.Infof("I2C configuration enabled. Initiating automatic reboot...") 578 | go rpiutils.PerformReboot(b.logger) 579 | } 580 | } 581 | 582 | func (b *pinctrlpi5) updateI2CConfig(desiredValue string) (bool, error) { 583 | configPath := rpiutils.GetBootConfigPath() 584 | return rpiutils.UpdateConfigFile(configPath, "dtparam=i2c_arm", desiredValue, b.logger) 585 | } 586 | 587 | func (b *pinctrlpi5) updateI2CModule(enable bool) (bool, error) { 588 | return rpiutils.UpdateModuleFile("/etc/modules", "i2c-dev", enable, b.logger) 589 | } 590 | 591 | // Close attempts to cleanly close each part of the board. 592 | func (b *pinctrlpi5) Close(ctx context.Context) error { 593 | b.mu.Lock() 594 | err := b.boardPinCtrl.Close() 595 | if err != nil { 596 | return fmt.Errorf("trouble cleaning up pincontrol memory: %w", err) 597 | } 598 | b.cancelFunc() 599 | b.mu.Unlock() 600 | b.activeBackgroundWorkers.Wait() 601 | 602 | for _, pin := range b.gpios { 603 | err = multierr.Combine(err, pin.Close()) 604 | } 605 | for _, interrupt := range b.interrupts { 606 | err = multierr.Combine(err, interrupt.Close()) 607 | } 608 | return err 609 | } 610 | --------------------------------------------------------------------------------