Skip to content

Commit

Permalink
gpioioctl:Implement ioctl access to Linux GPIO chips/lines. (#59)
Browse files Browse the repository at this point in the history
Includes new unit test that are conditionally compiled on linux. Mock in case of no chip available.
  • Loading branch information
gsexton committed Sep 18, 2024
1 parent 1e513a0 commit b69b28c
Show file tree
Hide file tree
Showing 10 changed files with 1,497 additions and 3 deletions.
7 changes: 7 additions & 0 deletions gpioioctl/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# GPIO IOCTL

This directory contains an implementation for Linux GPIO manipulation
using the ioctl v2 interface.

Basic test is provided, but a much more complete smoke test is provided
in periph.io/x/cmd/periph-smoketest/gpiosmoketest
140 changes: 140 additions & 0 deletions gpioioctl/basic_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// Copyright 2024 The Periph Authors. All rights reserved.
// Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file.

// Basic tests. More complete test is contained in the
// periph.io/x/cmd/periph-smoketest/gpiosmoketest
// folder.

//go:build linux

package gpioioctl

import (
"log"
"testing"

"periph.io/x/conn/v3/gpio"
"periph.io/x/conn/v3/gpio/gpioreg"
)

var testLine *GPIOLine

func init() {
var err error

if len(Chips) == 0 {
/*
During pipeline builds, GPIOChips may not be available, or
it may build on another OS. In that case, mock in enough
for a test to pass.
*/
line := GPIOLine{
number: 0,
name: "DummyGPIOLine",
consumer: "",
edge: gpio.NoEdge,
pull: gpio.PullNoChange,
direction: LineDirNotSet,
}

chip := GPIOChip{name: "DummyGPIOChip",
path: "/dev/gpiochipdummy",
label: "Dummy GPIOChip for Testing Purposes",
lineCount: 1,
lines: []*GPIOLine{&line},
}
Chips = append(Chips, &chip)
if err = gpioreg.Register(&line); err != nil {
nameStr := chip.Name()
lineStr := line.String()
log.Println("chip", nameStr, " gpioreg.Register(line) ", lineStr, " returned ", err)
}
}
}

func TestChips(t *testing.T) {
chip := Chips[0]
t.Log(chip.String())
if len(chip.Name()) == 0 {
t.Error("chip.Name() is 0 length")
}
if len(chip.Path()) == 0 {
t.Error("chip path is 0 length")
}
if len(chip.Label()) == 0 {
t.Error("chip label is 0 length!")
}
if len(chip.Lines()) != chip.LineCount() {
t.Errorf("Incorrect line count. Found: %d for LineCount, Returned Lines length=%d", chip.LineCount(), len(chip.Lines()))
}
for _, line := range chip.Lines() {
if len(line.Consumer()) == 0 && len(line.Name()) > 0 {
testLine = line
break
}
}
if testLine == nil {
t.Error("Error finding unused line for testing!")
}
for _, c := range Chips {
s := c.String()
if len(s) == 0 {
t.Error("Error calling chip.String(). No output returned!")
} else {
t.Log(s)
}

}

}

func TestGPIORegistryByName(t *testing.T) {
if testLine == nil {
return
}
outLine := gpioreg.ByName(testLine.Name())
if outLine == nil {
t.Fatalf("Error retrieving GPIO Line %s", testLine.Name())
}
if outLine.Name() != testLine.Name() {
t.Errorf("Error checking name. Expected %s, received %s", testLine.Name(), outLine.Name())
}

if outLine.Number() < 0 || outLine.Number() >= len(Chips[0].Lines()) {
t.Errorf("Invalid chip number %d received for %s", outLine.Number(), testLine.Name())
}
}

func TestNumber(t *testing.T) {
chip := Chips[0]
if testLine == nil {
return
}
l := chip.ByName(testLine.Name())
if l == nil {
t.Fatalf("Error retrieving GPIO Line %s", testLine.Name())
}
if l.Number() < 0 || l.Number() >= chip.LineCount() {
t.Errorf("line.Number() returned value (%d) out of range", l.Number())
}
l2 := chip.ByNumber(l.Number())
if l2 == nil {
t.Errorf("retrieve Line from chip by number %d failed.", l.Number())
}

}

func TestString(t *testing.T) {
if testLine == nil {
return
}
line := gpioreg.ByName(testLine.Name())
if line == nil {
t.Fatalf("Error retrieving GPIO Line %s", testLine.Name())
}
s := line.String()
if len(s) == 0 {
t.Errorf("GPIOLine.String() failed.")
}
}
15 changes: 15 additions & 0 deletions gpioioctl/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright 2024 The Periph Authors. All rights reserved.
// Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file.
//
// Package gpioioctl provides access to Linux GPIO lines using the ioctl interface.
//
// https://docs.kernel.org/userspace-api/gpio/index.html
//
// GPIO Pins can be accessed via periph.io/x/conn/v3/gpio/gpioreg,
// or using the Chips collection to access the specific GPIO chip
// and using it's ByName()/ByNumber methods.
//
// GPIOChip provides a LineSet feature that allows you to atomically
// read/write to multiple GPIO pins as a single operation.
package gpioioctl
105 changes: 105 additions & 0 deletions gpioioctl/example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
//go:build linux

package gpioioctl_test

// Copyright 2024 The Periph Authors. All rights reserved.
// Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file.

import (
"fmt"
"log"
"time"

"periph.io/x/conn/v3/driver/driverreg"
"periph.io/x/conn/v3/gpio"
"periph.io/x/conn/v3/gpio/gpioreg"
"periph.io/x/host/v3"
"periph.io/x/host/v3/gpioioctl"
)

func Example() {
_, _ = host.Init()
_, _ = driverreg.Init()

fmt.Println("GPIO Test Program")
chip := gpioioctl.Chips[0]
defer chip.Close()
fmt.Println(chip.String())
// Test by flashing an LED.
led := gpioreg.ByName("GPIO5")
fmt.Println("Flashing LED ", led.Name())
for i := range 20 {
_ = led.Out((i % 2) == 0)
time.Sleep(500 * time.Millisecond)
}
_ = led.Out(true)

testRotary(chip, "GPIO20", "GPIO21", "GPIO19")
}

// Test the LineSet functionality by using it to read a Rotary Encoder w/ Button.
func testRotary(chip *gpioioctl.GPIOChip, stateLine, dataLine, buttonLine string) {
config := gpioioctl.LineSetConfig{DefaultDirection: gpioioctl.LineInput, DefaultEdge: gpio.RisingEdge, DefaultPull: gpio.PullUp}
config.Lines = []string{stateLine, dataLine, buttonLine}
// The Data Pin of the Rotary Encoder should NOT have an edge.
_ = config.AddOverrides(gpioioctl.LineInput, gpio.NoEdge, gpio.PullUp, dataLine)
ls, err := chip.LineSetFromConfig(&config)
if err != nil {
log.Fatal(err)
}
defer ls.Close()
statePinNumber := uint32(ls.ByOffset(0).Number())
buttonPinNumber := uint32(ls.ByOffset(2).Number())

var tLast = time.Now().Add(-1 * time.Second)
var halting bool
go func() {
time.Sleep(60 * time.Second)
halting = true
fmt.Println("Sending halt!")
_ = ls.Halt()
}()
fmt.Println("Test Rotary Switch - Turn dial to test rotary encoder, press button to test it.")
for {
lineNumber, _, err := ls.WaitForEdge(0)
if err == nil {
tNow := time.Now()
if (tNow.UnixMilli() - tLast.UnixMilli()) < 100 {
continue
}
tLast = tNow
if lineNumber == statePinNumber {
var bits uint64
tDeadline := tNow.UnixNano() + 20_000_000
var consecutive uint64
for time.Now().UnixNano() < tDeadline {
// Spin on reading the pins until we get some number
// of consecutive readings that are the same.
bits, _ = ls.Read(0x03)
if bits&0x01 == 0x00 {
// We're bouncing.
consecutive = 0
} else {
consecutive += 1
if consecutive > 25 {
if bits == 0x01 {
fmt.Printf("Clockwise bits=%d\n", bits)
} else if bits == 0x03 {
fmt.Printf("CounterClockwise bits=%d\n", bits)
}
break
}
}
}
} else if lineNumber == buttonPinNumber {
fmt.Println("Button Pressed!")
}
} else {
fmt.Println("Timeout detected")
if halting {
break
}
}
}
}
Loading

0 comments on commit b69b28c

Please sign in to comment.