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 }