gobot.io/x/gobot/v2@v2.1.0/system/digitalpin_gpiod.go (about)

     1  package system
     2  
     3  import (
     4  	"fmt"
     5  	"log"
     6  	"strconv"
     7  	"strings"
     8  	"time"
     9  
    10  	"github.com/warthog618/gpiod"
    11  	"gobot.io/x/gobot/v2"
    12  )
    13  
    14  const systemGpiodDebug = false
    15  
    16  type cdevLine interface {
    17  	SetValue(value int) error
    18  	Value() (int, error)
    19  	Close() error
    20  }
    21  
    22  type digitalPinGpiod struct {
    23  	chipName string
    24  	pin      int
    25  	*digitalPinConfig
    26  	line cdevLine
    27  }
    28  
    29  var digitalPinGpiodReconfigure = digitalPinGpiodReconfigureLine // to allow unit testing
    30  
    31  var digitalPinGpiodUsed = map[bool]string{true: "used", false: "unused"}
    32  var digitalPinGpiodActiveLow = map[bool]string{true: "low", false: "high"}
    33  var digitalPinGpiodDebounced = map[bool]string{true: "debounced", false: "not debounced"}
    34  
    35  var digitalPinGpiodDirection = map[gpiod.LineDirection]string{gpiod.LineDirectionUnknown: "unknown direction",
    36  	gpiod.LineDirectionInput: "input", gpiod.LineDirectionOutput: "output"}
    37  
    38  var digitalPinGpiodDrive = map[gpiod.LineDrive]string{gpiod.LineDrivePushPull: "push-pull", gpiod.LineDriveOpenDrain: "open-drain",
    39  	gpiod.LineDriveOpenSource: "open-source"}
    40  
    41  var digitalPinGpiodBias = map[gpiod.LineBias]string{gpiod.LineBiasUnknown: "unknown", gpiod.LineBiasDisabled: "disabled",
    42  	gpiod.LineBiasPullUp: "pull-up", gpiod.LineBiasPullDown: "pull-down"}
    43  
    44  var digitalPinGpiodEdgeDetect = map[gpiod.LineEdge]string{gpiod.LineEdgeNone: "no", gpiod.LineEdgeRising: "rising",
    45  	gpiod.LineEdgeFalling: "falling", gpiod.LineEdgeBoth: "both"}
    46  
    47  var digitalPinGpiodEventClock = map[gpiod.LineEventClock]string{gpiod.LineEventClockMonotonic: "monotonic",
    48  	gpiod.LineEventClockRealtime: "realtime"}
    49  
    50  // newDigitalPinGpiod returns a digital pin given the pin number, with the label "gobotio" followed by the pin number.
    51  // The pin label can be modified optionally. The pin is handled by the character device Kernel ABI.
    52  func newDigitalPinGpiod(chipName string, pin int, options ...func(gobot.DigitalPinOptioner) bool) *digitalPinGpiod {
    53  	if chipName == "" {
    54  		chipName = "gpiochip0"
    55  	}
    56  	cfg := newDigitalPinConfig("gobotio"+strconv.Itoa(int(pin)), options...)
    57  	d := &digitalPinGpiod{
    58  		chipName:         chipName,
    59  		pin:              pin,
    60  		digitalPinConfig: cfg,
    61  	}
    62  	return d
    63  }
    64  
    65  // ApplyOptions apply all given options to the pin immediately. Implements interface gobot.DigitalPinOptionApplier.
    66  func (d *digitalPinGpiod) ApplyOptions(options ...func(gobot.DigitalPinOptioner) bool) error {
    67  	anyChange := false
    68  	for _, option := range options {
    69  		anyChange = option(d) || anyChange
    70  	}
    71  	if anyChange {
    72  		return digitalPinGpiodReconfigure(d, false)
    73  	}
    74  	return nil
    75  }
    76  
    77  // DirectionBehavior gets the direction behavior when the pin is used the next time. This means its possibly not in
    78  // this direction type at the moment. Implements the interface gobot.DigitalPinValuer, but should be rarely used.
    79  func (d *digitalPinGpiod) DirectionBehavior() string {
    80  	return d.direction
    81  }
    82  
    83  // Export sets the pin as used by this driver. Implements the interface gobot.DigitalPinner.
    84  func (d *digitalPinGpiod) Export() error {
    85  	err := digitalPinGpiodReconfigure(d, false)
    86  	if err != nil {
    87  		return fmt.Errorf("gpiod.Export(): %v", err)
    88  	}
    89  	return nil
    90  }
    91  
    92  // Unexport releases the pin as input. Implements the interface gobot.DigitalPinner.
    93  func (d *digitalPinGpiod) Unexport() error {
    94  	var errs []string
    95  	if d.line != nil {
    96  		if err := digitalPinGpiodReconfigure(d, true); err != nil {
    97  			errs = append(errs, err.Error())
    98  		}
    99  		if err := d.line.Close(); err != nil {
   100  			err = fmt.Errorf("gpiod.Unexport()-line.Close(): %v", err)
   101  			errs = append(errs, err.Error())
   102  		}
   103  	}
   104  	if len(errs) == 0 {
   105  		return nil
   106  	}
   107  	return fmt.Errorf(strings.Join(errs, ","))
   108  }
   109  
   110  // Write writes the given value to the character device. Implements the interface gobot.DigitalPinner.
   111  func (d *digitalPinGpiod) Write(val int) error {
   112  	if val < 0 {
   113  		val = 0
   114  	}
   115  	if val > 1 {
   116  		val = 1
   117  	}
   118  
   119  	err := d.line.SetValue(val)
   120  	if err != nil {
   121  		return fmt.Errorf("gpiod.Write(): %v", err)
   122  	}
   123  	return nil
   124  }
   125  
   126  // Read reads the given value from character device. Implements the interface gobot.DigitalPinner.
   127  func (d *digitalPinGpiod) Read() (int, error) {
   128  	val, err := d.line.Value()
   129  	if err != nil {
   130  		return 0, fmt.Errorf("gpiod.Read(): %v", err)
   131  	}
   132  	return val, err
   133  }
   134  
   135  // ListLines is used for development purposes.
   136  func (d *digitalPinGpiod) ListLines() error {
   137  	c, err := gpiod.NewChip(d.chipName, gpiod.WithConsumer(d.label))
   138  	if err != nil {
   139  		return err
   140  	}
   141  	for i := 0; i < c.Lines(); i++ {
   142  		li, err := c.LineInfo(i)
   143  		if err != nil {
   144  			return err
   145  		}
   146  		fmt.Println(digitalPinGpiodFmtLine(li))
   147  	}
   148  
   149  	return nil
   150  }
   151  
   152  // List is used for development purposes.
   153  func (d *digitalPinGpiod) List() error {
   154  	c, err := gpiod.NewChip(d.chipName)
   155  	if err != nil {
   156  		return err
   157  	}
   158  	defer c.Close()
   159  	l, err := c.RequestLine(d.pin)
   160  	if err != nil && l != nil {
   161  		l.Close()
   162  		l = nil
   163  	}
   164  	li, err := l.Info()
   165  	if err != nil {
   166  		return err
   167  	}
   168  	fmt.Println(digitalPinGpiodFmtLine(li))
   169  
   170  	return nil
   171  }
   172  
   173  func digitalPinGpiodReconfigureLine(d *digitalPinGpiod, forceInput bool) error {
   174  	// cleanup old line
   175  	if d.line != nil {
   176  		d.line.Close()
   177  	}
   178  	d.line = nil
   179  
   180  	// acquire chip, temporary
   181  	// the given label is applied to all lines, which are requested on the chip
   182  	gpiodChip, err := gpiod.NewChip(d.chipName, gpiod.WithConsumer(d.label))
   183  	id := fmt.Sprintf("%s-%d", d.chipName, d.pin)
   184  	if err != nil {
   185  		return fmt.Errorf("gpiod.reconfigure(%s)-lib.NewChip(%s): %v", id, d.chipName, err)
   186  	}
   187  	defer gpiodChip.Close()
   188  
   189  	// collect line configuration options
   190  	var opts []gpiod.LineReqOption
   191  
   192  	// configure direction, debounce period (inputs only), edge detection (inputs only) and drive (outputs only)
   193  	if d.direction == IN || forceInput {
   194  		if systemGpiodDebug {
   195  			log.Printf("input (%s): debounce %s, edge %d, handler %t, inverse %t, bias %d",
   196  				id, d.debouncePeriod, d.edge, d.edgeEventHandler != nil, d.activeLow, d.bias)
   197  		}
   198  		opts = append(opts, gpiod.AsInput)
   199  		if !forceInput && d.drive != digitalPinDrivePushPull && systemGpiodDebug {
   200  			log.Printf("\n++ drive option (%d) is dropped for input++\n", d.drive)
   201  		}
   202  		if d.debouncePeriod != 0 {
   203  			opts = append(opts, gpiod.WithDebounce(d.debouncePeriod))
   204  		}
   205  		// edge detection
   206  		if d.edgeEventHandler != nil {
   207  			wrappedHandler := digitalPinGpiodGetWrappedEventHandler(d.edgeEventHandler)
   208  			switch d.edge {
   209  			case digitalPinEventOnFallingEdge:
   210  				opts = append(opts, gpiod.WithEventHandler(wrappedHandler), gpiod.WithFallingEdge)
   211  			case digitalPinEventOnRisingEdge:
   212  				opts = append(opts, gpiod.WithEventHandler(wrappedHandler), gpiod.WithRisingEdge)
   213  			case digitalPinEventOnBothEdges:
   214  				opts = append(opts, gpiod.WithEventHandler(wrappedHandler), gpiod.WithBothEdges)
   215  			default:
   216  				opts = append(opts, gpiod.WithoutEdges)
   217  			}
   218  		}
   219  	} else {
   220  		if systemGpiodDebug {
   221  			log.Printf("ouput (%s): ini-state %d, drive %d, inverse %t, bias %d",
   222  				id, d.outInitialState, d.drive, d.activeLow, d.bias)
   223  		}
   224  		opts = append(opts, gpiod.AsOutput(d.outInitialState))
   225  		switch d.drive {
   226  		case digitalPinDriveOpenDrain:
   227  			opts = append(opts, gpiod.AsOpenDrain)
   228  		case digitalPinDriveOpenSource:
   229  			opts = append(opts, gpiod.AsOpenSource)
   230  		default:
   231  			opts = append(opts, gpiod.AsPushPull)
   232  		}
   233  		if d.debouncePeriod != 0 && systemGpiodDebug {
   234  			log.Printf("\n++debounce option (%d) is dropped for output++\n", d.drive)
   235  		}
   236  		if d.edgeEventHandler != nil || d.edge != digitalPinEventNone && systemGpiodDebug {
   237  			log.Printf("\n++edge detection is dropped for output++\n")
   238  		}
   239  	}
   240  
   241  	// configure inverse logic (inputs and outputs)
   242  	if d.activeLow {
   243  		opts = append(opts, gpiod.AsActiveLow)
   244  	}
   245  
   246  	// configure bias (inputs and outputs)
   247  	switch d.bias {
   248  	case digitalPinBiasPullDown:
   249  		opts = append(opts, gpiod.WithPullDown)
   250  	case digitalPinBiasPullUp:
   251  		opts = append(opts, gpiod.WithPullUp)
   252  	default:
   253  		opts = append(opts, gpiod.WithBiasAsIs)
   254  	}
   255  
   256  	// acquire line with collected options
   257  	gpiodLine, err := gpiodChip.RequestLine(d.pin, opts...)
   258  	if err != nil {
   259  		if gpiodLine != nil {
   260  			gpiodLine.Close()
   261  		}
   262  		d.line = nil
   263  
   264  		return fmt.Errorf("gpiod.reconfigure(%s)-c.RequestLine(%d, %v): %v", id, d.pin, opts, err)
   265  	}
   266  	d.line = gpiodLine
   267  
   268  	return nil
   269  }
   270  
   271  func digitalPinGpiodGetWrappedEventHandler(handler func(int, time.Duration, string, uint32, uint32)) func(gpiod.LineEvent) {
   272  	return func(evt gpiod.LineEvent) {
   273  		detectedEdge := "none"
   274  		switch evt.Type {
   275  		case gpiod.LineEventRisingEdge:
   276  			detectedEdge = DigitalPinEventRisingEdge
   277  		case gpiod.LineEventFallingEdge:
   278  			detectedEdge = DigitalPinEventFallingEdge
   279  		}
   280  		handler(evt.Offset, evt.Timestamp, detectedEdge, evt.Seqno, evt.LineSeqno)
   281  	}
   282  }
   283  
   284  func digitalPinGpiodFmtLine(li gpiod.LineInfo) string {
   285  	var consumer string
   286  	if li.Consumer != "" {
   287  		consumer = fmt.Sprintf(" by '%s'", li.Consumer)
   288  	}
   289  	return fmt.Sprintf("++ Info line %d '%s', %s%s ++\n Config: %s\n",
   290  		li.Offset, li.Name, digitalPinGpiodUsed[li.Used], consumer, digitalPinGpiodFmtLineConfig(li.Config))
   291  }
   292  
   293  func digitalPinGpiodFmtLineConfig(cfg gpiod.LineConfig) string {
   294  	t := "active-%s, %s, %s, %s bias, %s edge detect, %s, debounce-period: %v, %s event clock"
   295  	return fmt.Sprintf(t, digitalPinGpiodActiveLow[cfg.ActiveLow], digitalPinGpiodDirection[cfg.Direction],
   296  		digitalPinGpiodDrive[cfg.Drive], digitalPinGpiodBias[cfg.Bias], digitalPinGpiodEdgeDetect[cfg.EdgeDetection],
   297  		digitalPinGpiodDebounced[cfg.Debounced], cfg.DebouncePeriod, digitalPinGpiodEventClock[cfg.EventClock])
   298  }