From 6e12bd9d59d15a55e7cfe7cc5eed4185faaee812 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Mar 2026 12:32:33 +0100 Subject: [PATCH] Gpio plugin: Raspberry Pi 5 compatibility (#27815) --- charger/openwb-native_linux.go | 64 +++++++++++++++++++--------------- go.mod | 2 +- go.sum | 6 ++-- plugin/gpio_linux.go | 59 ++++++++++++++++--------------- 4 files changed, 71 insertions(+), 60 deletions(-) diff --git a/charger/openwb-native_linux.go b/charger/openwb-native_linux.go index 7ecb7bad1..9455a9caa 100644 --- a/charger/openwb-native_linux.go +++ b/charger/openwb-native_linux.go @@ -9,12 +9,18 @@ import ( "github.com/evcc-io/evcc/charger/openwb/native" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/modbus" - "github.com/fatih/structs" - "github.com/stianeikeland/go-rpio/v4" + "github.com/warthog618/go-gpiocdev" ) const minCpWaitTime time.Duration = 5 * time.Second +// openWbGpioLines holds GPIO lines for a single charge point +type openWbGpioLines struct { + cp *gpiocdev.Line + ph1 *gpiocdev.Line + ph3 *gpiocdev.Line +} + // OpenWbNative charger implementation type OpenWbNative struct { api.Charger @@ -23,6 +29,7 @@ type OpenWbNative struct { cpWait time.Duration connector int chargeState api.ChargeStatus + gpio openWbGpioLines } // gpioAction defines a single GPIO pin operation with timing @@ -44,8 +51,10 @@ func NewOpenWbNativeFromConfig(ctx context.Context, other map[string]any) (api.C RfId string CpWait time.Duration Connector int + Chip string modbus.Settings `mapstructure:",squash"` }{ + Chip: "gpiochip0", Settings: modbus.Settings{ Baudrate: 9600, Comset: "8N1", @@ -64,11 +73,11 @@ func NewOpenWbNativeFromConfig(ctx context.Context, other map[string]any) (api.C return nil, fmt.Errorf("invalid cpwait value: %v, needs to be greater than %s", cc.CpWait, minCpWaitTime) } - return NewOpenWbNative(ctx, cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.Protocol(), cc.ID, cc.Phases1p3p, cc.RfId, cc.CpWait, cc.Connector) + return NewOpenWbNative(ctx, cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.Protocol(), cc.ID, cc.Phases1p3p, cc.RfId, cc.CpWait, cc.Connector, cc.Chip) } // NewOpenWbNative creates OpenWbNative charger -func NewOpenWbNative(ctx context.Context, uri, device, comset string, baudrate int, proto modbus.Protocol, slaveID uint8, hasPhases1p3p bool, rfIdVidPid string, cpWait time.Duration, connector int) (api.Charger, error) { +func NewOpenWbNative(ctx context.Context, uri, device, comset string, baudrate int, proto modbus.Protocol, slaveID uint8, hasPhases1p3p bool, rfIdVidPid string, cpWait time.Duration, connector int, chip string) (api.Charger, error) { log := util.NewLogger("openwb-native") log.DEBUG.Printf("Creating OpenWB native with 3 phases %t, rfid %s, cpwait %s, connector %d", hasPhases1p3p, rfIdVidPid, cpWait.String(), connector) @@ -109,14 +118,21 @@ func NewOpenWbNative(ctx context.Context, uri, device, comset string, baudrate i identify = wb.identify } - // initialize GPIO and set pins to output - if err := rpio.Open(); err != nil { - return nil, fmt.Errorf("failed to open GPIO: %w", err) - } - defer rpio.Close() - - for _, pin := range structs.Fields(native.ChargePoints[connector-1]) { - rpio.Pin(pin.Value().(int)).Output() + // initialize GPIO lines and set pins to output + pins := native.ChargePoints[connector-1] + for _, gpioConfig := range []struct { + dst **gpiocdev.Line + pin int + }{ + {&wb.gpio.cp, pins.PIN_CP}, + {&wb.gpio.ph1, pins.PIN_1P}, + {&wb.gpio.ph3, pins.PIN_3P}, + } { + line, err := gpiocdev.RequestLine(chip, gpioConfig.pin, gpiocdev.AsOutput(0)) + if err != nil { + return nil, fmt.Errorf("failed to open GPIO pin %d: %w", gpioConfig.pin, err) + } + *gpioConfig.dst = line } return decorateOpenWbNative(wb, maxCurrentMillis, phases1p3p, identify), nil @@ -147,21 +163,14 @@ var _ api.Resurrector = (*OpenWbNative)(nil) // WakeUp implements the api.Resurrector interface func (wb *OpenWbNative) WakeUp() error { - cpPin := rpio.Pin(native.ChargePoints[wb.connector-1].PIN_CP) - return wb.runGpioSequence([]gpioAction{ - {pin: cpPin.High, delay: wb.cpWait}, - {pin: cpPin.Low, delay: 0}, + {pin: func() { wb.gpio.cp.SetValue(1) }, delay: wb.cpWait}, + {pin: func() { wb.gpio.cp.SetValue(0) }, delay: 0}, }) } // runGpioSequence executes a sequence of GPIO operations func (wb *OpenWbNative) runGpioSequence(seq []gpioAction) error { - if err := rpio.Open(); err != nil { - return fmt.Errorf("failed to open GPIO: %w", err) - } - defer rpio.Close() - if err := wb.Enable(false); err != nil { return err } @@ -178,17 +187,16 @@ func (wb *OpenWbNative) runGpioSequence(seq []gpioAction) error { // gpioSwitchPhases toggles the GPIOs to switch between 1-phase and 3-phase charging func (wb *OpenWbNative) gpioSwitchPhases(phases int) error { - cpPin := rpio.Pin(native.ChargePoints[wb.connector-1].PIN_CP) - phPin := rpio.Pin(native.ChargePoints[wb.connector-1].PIN_3P) + phLine := wb.gpio.ph3 if phases == 1 { - phPin = rpio.Pin(native.ChargePoints[wb.connector-1].PIN_1P) + phLine = wb.gpio.ph1 } return wb.runGpioSequence([]gpioAction{ - {pin: cpPin.High, delay: time.Second}, // enable phases switch relay (NO), disconnect CP - {pin: phPin.High, delay: wb.cpWait / 2}, // move latching relay to desired position - {pin: phPin.Low, delay: wb.cpWait / 2}, // lock latching relay - {pin: cpPin.Low, delay: time.Second}, // disable phase switching, reconnect CP + {pin: func() { wb.gpio.cp.SetValue(1) }, delay: time.Second}, // enable phases switch relay (NO), disconnect CP + {pin: func() { phLine.SetValue(1) }, delay: wb.cpWait / 2}, // move latching relay to desired position + {pin: func() { phLine.SetValue(0) }, delay: wb.cpWait / 2}, // lock latching relay + {pin: func() { wb.gpio.cp.SetValue(0) }, delay: time.Second}, // disable phase switching, reconnect CP }) } diff --git a/go.mod b/go.mod index 06b86b14b..a163e02d2 100644 --- a/go.mod +++ b/go.mod @@ -94,12 +94,12 @@ require ( github.com/spf13/jwalterweatherman v1.1.0 github.com/spf13/pflag v1.0.10 github.com/spf13/viper v1.21.0 - github.com/stianeikeland/go-rpio/v4 v4.6.0 github.com/stretchr/testify v1.11.1 github.com/teslamotors/vehicle-command v0.4.1 github.com/tess1o/go-ecoflow v1.1.1-0.20251003083510-2ccc15a17e29 github.com/traefik/yaegi v0.16.1 github.com/volkszaehler/mbmd v0.0.0-20260131091050-86c2d25b6103 + github.com/warthog618/go-gpiocdev v0.9.1 gitlab.com/bboehmke/sunny v0.16.0 go.bug.st/serial v1.6.4 go.uber.org/mock v0.6.0 diff --git a/go.sum b/go.sum index 69e15bc1c..7212cc09a 100644 --- a/go.sum +++ b/go.sum @@ -727,8 +727,6 @@ github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3A github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= -github.com/stianeikeland/go-rpio/v4 v4.6.0 h1:eAJgtw3jTtvn/CqwbC82ntcS+dtzUTgo5qlZKe677EY= -github.com/stianeikeland/go-rpio/v4 v4.6.0/go.mod h1:A3GvHxC1Om5zaId+HqB3HKqx4K/AqeckxB7qRjxMK7o= github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= @@ -773,6 +771,10 @@ github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAh github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/volkszaehler/mbmd v0.0.0-20260131091050-86c2d25b6103 h1:I91QVys5UwYJ8G7VVQDeXN2/BPZqEeqCp7NY65BmGTU= github.com/volkszaehler/mbmd v0.0.0-20260131091050-86c2d25b6103/go.mod h1:ZkD3hHsxHxdD2KbqQrMXQd1veeFfggok+breXT+TqWQ= +github.com/warthog618/go-gpiocdev v0.9.1 h1:pwHPaqjJfhCipIQl78V+O3l9OKHivdRDdmgXYbmhuCI= +github.com/warthog618/go-gpiocdev v0.9.1/go.mod h1:dN3e3t/S2aSNC+hgigGE/dBW8jE1ONk9bDSEYfoPyl8= +github.com/warthog618/go-gpiosim v0.1.1 h1:MRAEv+T+itmw+3GeIGpQJBfanUVyg0l3JCTwHtwdre4= +github.com/warthog618/go-gpiosim v0.1.1/go.mod h1:YXsnB+I9jdCMY4YAlMSRrlts25ltjmuIsrnoUrBLdqU= github.com/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc= github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= diff --git a/plugin/gpio_linux.go b/plugin/gpio_linux.go index 2d63b9e67..404665f2e 100644 --- a/plugin/gpio_linux.go +++ b/plugin/gpio_linux.go @@ -8,7 +8,7 @@ import ( "sync" "github.com/evcc-io/evcc/util" - "github.com/stianeikeland/go-rpio/v4" + "github.com/warthog618/go-gpiocdev" ) func init() { @@ -16,44 +16,44 @@ func init() { } type gpio struct { - mu sync.Mutex - typ GpioType - pin rpio.Pin + mu sync.Mutex + typ GpioType + line *gpiocdev.Line } // NewGpioPluginFromConfig creates a GPIO provider func NewGpioPluginFromConfig(ctx context.Context, other map[string]any) (Plugin, error) { - var cc struct { + cc := struct { Function GpioType Pin int + Chip string + }{ + Chip: "gpiochip0", } if err := util.DecodeOther(other, &cc); err != nil { return nil, err } - p := &gpio{ - typ: cc.Function, - pin: rpio.Pin(cc.Pin), - } - - // initialize GPIO and set pins to input - if err := rpio.Open(); err != nil { - return nil, fmt.Errorf("failed to open GPIO: %w", err) - } - defer rpio.Close() - + var opts []gpiocdev.LineReqOption switch cc.Function { case GpioTypeRead: - p.pin.Input() - p.pin.PullUp() + opts = append(opts, gpiocdev.AsInput, gpiocdev.WithPullUp) case GpioTypeWrite: - p.pin.Output() + opts = append(opts, gpiocdev.AsOutput(0)) default: return nil, fmt.Errorf("invalid type: %s", cc.Function) } - return p, nil + line, err := gpiocdev.RequestLine(cc.Chip, cc.Pin, opts...) + if err != nil { + return nil, fmt.Errorf("failed to open GPIO: %w", err) + } + + return &gpio{ + typ: cc.Function, + line: line, + }, nil } var _ BoolGetter = (*gpio)(nil) @@ -68,12 +68,12 @@ func (p *gpio) BoolGetter() (func() (bool, error), error) { p.mu.Lock() defer p.mu.Unlock() - if err := rpio.Open(); err != nil { - return false, fmt.Errorf("failed to open GPIO: %w", err) + val, err := p.line.Value() + if err != nil { + return false, fmt.Errorf("failed to read GPIO: %w", err) } - defer rpio.Close() - return p.pin.Read() != rpio.Low, nil + return val != 0, nil }, nil } @@ -89,13 +89,14 @@ func (p *gpio) BoolSetter(_ string) (func(bool) error, error) { p.mu.Lock() defer p.mu.Unlock() - if err := rpio.Open(); err != nil { - return fmt.Errorf("failed to open GPIO: %w", err) + val := 0 + if b { + val = 1 } - defer rpio.Close() - val := map[bool]rpio.State{false: rpio.Low, true: rpio.High}[b] - p.pin.Write(val) + if err := p.line.SetValue(val); err != nil { + return fmt.Errorf("failed to write GPIO: %w", err) + } return nil }, nil