hybridgroup.gobot/system/digitalpin_sysfs.go

236 lines
5.7 KiB
Go

package system
import (
"errors"
"fmt"
"log"
"os"
"strconv"
"time"
"gobot.io/x/gobot/v2"
)
const (
systemSysfsDebug = false
// gpioPath default linux sysfs gpio path
gpioPath = "/sys/class/gpio"
)
var errNotExported = errors.New("pin has not been exported")
// digitalPin represents a digital pin
type digitalPinSysfs struct {
pin string
*digitalPinConfig
sfa *sysfsFileAccess
dirFile *sysfsFile
valFile *sysfsFile
activeLowFile *sysfsFile
}
// newDigitalPinSysfs returns a digital pin using for the given number. The name of the sysfs file will prepend "gpio"
// to the pin number, eg. a pin number of 10 will have a name of "gpio10". The pin is handled by the sysfs Kernel ABI.
func newDigitalPinSysfs(
sfa *sysfsFileAccess,
pin string,
options ...func(gobot.DigitalPinOptioner) bool,
) *digitalPinSysfs {
cfg := newDigitalPinConfig("gpio"+pin, options...)
d := &digitalPinSysfs{
pin: pin,
digitalPinConfig: cfg,
sfa: sfa,
}
return d
}
// ApplyOptions apply all given options to the pin immediately. Implements interface gobot.DigitalPinOptionApplier.
func (d *digitalPinSysfs) ApplyOptions(options ...func(gobot.DigitalPinOptioner) bool) error {
anyChange := false
for _, option := range options {
anyChange = option(d) || anyChange
}
if anyChange {
return d.reconfigure()
}
return nil
}
// DirectionBehavior gets the direction behavior when the pin is used the next time. This means its possibly not in
// this direction type at the moment. Implements the interface gobot.DigitalPinValuer, but should be rarely used.
func (d *digitalPinSysfs) DirectionBehavior() string {
return d.direction
}
// Export sets the pin as exported with the configured direction
func (d *digitalPinSysfs) Export() error {
return d.reconfigure()
}
// Unexport release the pin
func (d *digitalPinSysfs) Unexport() error {
unexport, err := d.sfa.openWrite(gpioPath + "/unexport")
if err != nil {
return err
}
defer unexport.close()
if d.dirFile != nil {
d.dirFile.close()
d.dirFile = nil
}
if d.valFile != nil {
d.valFile.close()
d.valFile = nil
}
if d.activeLowFile != nil {
d.activeLowFile.close()
d.activeLowFile = nil
}
err = unexport.write([]byte(d.pin))
if err != nil {
// If EINVAL then the pin is reserved in the system and can't be unexported, we suppress the error
var pathError *os.PathError
if !(errors.As(err, &pathError) && errors.Is(err, Syscall_EINVAL)) {
return err
}
}
return nil
}
// Write writes the given value to the character device
func (d *digitalPinSysfs) Write(b int) error {
if d.valFile == nil {
return errNotExported
}
err := d.valFile.write([]byte(strconv.Itoa(b)))
return err
}
// Read reads a value from character device
func (d *digitalPinSysfs) Read() (int, error) {
if d.valFile == nil {
return 0, errNotExported
}
buf, err := d.valFile.read()
if err != nil {
return 0, err
}
return strconv.Atoi(string(buf[0]))
}
func (d *digitalPinSysfs) reconfigure() error {
exportFile, err := d.sfa.openWrite(gpioPath + "/export")
if err != nil {
return err
}
defer exportFile.close()
err = exportFile.write([]byte(d.pin))
if err != nil {
// If EBUSY then the pin has already been exported, we suppress the error
var pathError *os.PathError
if !(errors.As(err, &pathError) && errors.Is(err, Syscall_EBUSY)) {
return err
}
}
if d.dirFile != nil {
d.dirFile.close()
}
attempt := 0
for {
attempt++
d.dirFile, err = d.sfa.openReadWrite(fmt.Sprintf("%s/%s/direction", gpioPath, d.label))
if err == nil {
break
}
if attempt > 10 {
break
}
time.Sleep(10 * time.Millisecond)
}
if d.valFile != nil {
d.valFile.close()
}
if err == nil {
d.valFile, err = d.sfa.openReadWrite(fmt.Sprintf("%s/%s/value", gpioPath, d.label))
}
// configure direction
if err == nil {
err = d.writeDirectionWithInitialOutput()
}
// configure inverse logic
if err == nil {
if d.activeLow {
d.activeLowFile, err = d.sfa.openReadWrite(fmt.Sprintf("%s/%s/active_low", gpioPath, d.label))
if err == nil {
err = d.activeLowFile.write([]byte("1"))
}
}
}
// configure bias (inputs and outputs, unsupported)
if err == nil {
if d.bias != digitalPinBiasDefault && systemSysfsDebug {
log.Printf("bias options (%d) are not supported by sysfs, please use hardware resistors instead\n", d.bias)
}
}
// configure debounce period (inputs only), edge detection (inputs only) and drive (outputs only)
if d.direction == IN {
// configure debounce (unsupported)
if d.debouncePeriod != 0 && systemSysfsDebug {
log.Printf("debounce period option (%d) is not supported by sysfs\n", d.debouncePeriod)
}
// configure edge detection
if err == nil {
if d.edge != 0 && d.pollInterval <= 0 {
err = fmt.Errorf("edge detect option (%d) is not implemented for sysfs without discrete polling", d.edge)
}
}
// start discrete polling function and wait for first read is done
if err == nil {
if d.pollInterval > 0 {
err = startEdgePolling(d.label, d.Read, d.pollInterval, d.edge, d.edgeEventHandler, d.pollQuitChan)
}
}
} else if d.drive != digitalPinDrivePushPull && systemSysfsDebug {
// configure drive (unsupported)
log.Printf("drive options (%d) are not supported by sysfs\n", d.drive)
}
if err != nil {
if e := d.Unexport(); e != nil {
err = fmt.Errorf("unexport error '%v' after '%v'", e, err)
}
}
return err
}
func (d *digitalPinSysfs) writeDirectionWithInitialOutput() error {
if d.dirFile == nil {
return errNotExported
}
if err := d.dirFile.write([]byte(d.direction)); err != nil || d.direction == IN {
return err
}
if d.valFile == nil {
return errNotExported
}
return d.valFile.write([]byte(strconv.Itoa(d.outInitialState)))
}