Skip to content

Commit

Permalink
NRG: lock against concurrent requests (#16397)
Browse files Browse the repository at this point in the history
  • Loading branch information
andig committed Sep 28, 2024
1 parent a539310 commit c3eb5f7
Show file tree
Hide file tree
Showing 2 changed files with 77 additions and 78 deletions.
155 changes: 77 additions & 78 deletions charger/nrgble_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package charger
import (
"bytes"
"fmt"
"os"
"strconv"
"sync"
"time"

"github.com/evcc-io/evcc/api"
Expand All @@ -22,6 +22,7 @@ const nrgTimeout = 10 * time.Second

// NRGKickBLE charger implementation
type NRGKickBLE struct {
mu sync.Mutex
log *util.Logger
timer *time.Ticker
adapter *adapter.Adapter1
Expand Down Expand Up @@ -68,10 +69,6 @@ func NewNRGKickBLE(device, mac string, pin int) (*NRGKickBLE, error) {
// set LE mode
btmgmt := hw.NewBtMgmt(ainfo.AdapterID)

if len(os.Getenv("DOCKER")) > 0 {
btmgmt.BinPath = "./docker-btmgmt"
}

err = btmgmt.SetPowered(false)
if err == nil {
err = btmgmt.SetLe(true)
Expand Down Expand Up @@ -102,12 +99,11 @@ func NewNRGKickBLE(device, mac string, pin int) (*NRGKickBLE, error) {
agent.NextAgentPath()

ag := agent.NewSimpleAgent()
err = agent.ExposeAgent(conn, ag, agent.CapNoInputNoOutput, true)
if err != nil {
if err := agent.ExposeAgent(conn, ag, agent.CapNoInputNoOutput, true); err != nil {
return nil, err
}

nrg := &NRGKickBLE{
wb := &NRGKickBLE{
log: logger,
timer: time.NewTicker(2 * time.Second),
device: ainfo.AdapterID,
Expand All @@ -117,108 +113,111 @@ func NewNRGKickBLE(device, mac string, pin int) (*NRGKickBLE, error) {
agent: ag,
}

return nrg, nil
return wb, nil
}

func (nrg *NRGKickBLE) connect() (*device.Device1, error) {
dev, err := ble.FindDevice(nrg.adapter, nrg.mac, nrgTimeout)
func (wb *NRGKickBLE) connect() (*device.Device1, error) {
dev, err := ble.FindDevice(wb.adapter, wb.mac, nrgTimeout)
if err != nil {
return nil, fmt.Errorf("find device: %s", err)
}

if err := ble.Connect(dev, nrg.agent, nrg.device); err != nil {
return nil, err
}

return dev, nil
err = ble.Connect(dev, wb.agent, wb.device)
return dev, err
}

func (nrg *NRGKickBLE) close() {
if nrg.dev != nil {
nrg.dev.Close()
nrg.dev = nil
func (wb *NRGKickBLE) close() {
if wb.dev != nil {
wb.dev.Close()
wb.dev = nil
}
}

func (nrg *NRGKickBLE) read(service string, res interface{}) error {
<-nrg.timer.C
func (wb *NRGKickBLE) read(service string, res interface{}) error {
wb.mu.Lock()
defer wb.mu.Unlock()

<-wb.timer.C

if nrg.dev == nil {
dev, err := nrg.connect()
if wb.dev == nil {
dev, err := wb.connect()
if err != nil {
return err
}
nrg.dev = dev
wb.dev = dev
}

char, err := nrg.dev.GetCharByUUID(service)
char, err := wb.dev.GetCharByUUID(service)
if err != nil {
nrg.close()
wb.close()
return err
}

b, err := char.ReadValue(map[string]interface{}{})
if err != nil {
nrg.close()
wb.close()
return err
}
nrg.log.TRACE.Printf("read %s %0x", service, b)
wb.log.TRACE.Printf("read %s %0x", service, b)

return struc.Unpack(bytes.NewReader(b), res)
}

func (nrg *NRGKickBLE) write(service string, val interface{}) error {
func (wb *NRGKickBLE) write(service string, val interface{}) error {
wb.mu.Lock()
defer wb.mu.Unlock()

var out bytes.Buffer
if err := struc.Pack(&out, val); err != nil {
return err
}
nrg.log.TRACE.Printf("write %s %0x", service, out.Bytes())
wb.log.TRACE.Printf("write %s %0x", service, out.Bytes())

<-nrg.timer.C
<-wb.timer.C

if nrg.dev == nil {
dev, err := nrg.connect()
if wb.dev == nil {
dev, err := wb.connect()
if err != nil {
return err
}
nrg.dev = dev
wb.dev = dev
}

char, err := nrg.dev.GetCharByUUID(service)
char, err := wb.dev.GetCharByUUID(service)
if err != nil {
nrg.close()
wb.close()
return err
}

if err := char.WriteValue(out.Bytes(), map[string]interface{}{}); err != nil {
nrg.close()
wb.close()
return err
}

return nil
}

func (nrg *NRGKickBLE) mergeSettings(info ble.Info) ble.Settings {
func (wb *NRGKickBLE) mergeSettings(info ble.Info) ble.Settings {
return ble.Settings{
PIN: nrg.pin,
PIN: wb.pin,
ChargingEnergyLimit: 19997, // magic const for "disable"
KWhPer100: info.KWhPer100,
AmountPerKWh: info.AmountPerKWh,
Efficiency: info.Efficiency,
BLETransmissionPower: info.BLETransmissionPower,
PauseCharging: nrg.pauseCharging, // apply last value
Current: nrg.current, // apply last value
PauseCharging: wb.pauseCharging, // apply last value
Current: wb.current, // apply last value
}
}

// Status implements the api.Charger interface
func (nrg *NRGKickBLE) Status() (api.ChargeStatus, error) {
res := ble.Power{}
if err := nrg.read(ble.PowerService, &res); err != nil {
func (wb *NRGKickBLE) Status() (api.ChargeStatus, error) {
var res ble.Power
if err := wb.read(ble.PowerService, &res); err != nil {
return api.StatusF, err
}

nrg.log.TRACE.Printf("read power: %+v", res)
wb.log.TRACE.Printf("read power: %+v", res)

switch res.CPSignal {
case 3:
Expand All @@ -233,99 +232,99 @@ func (nrg *NRGKickBLE) Status() (api.ChargeStatus, error) {
}

// Enabled implements the api.Charger interface
func (nrg *NRGKickBLE) Enabled() (bool, error) {
res := ble.Info{}
if err := nrg.read(ble.InfoService, &res); err != nil {
func (wb *NRGKickBLE) Enabled() (bool, error) {
var res ble.Info
if err := wb.read(ble.InfoService, &res); err != nil {
return false, err
}

nrg.log.TRACE.Printf("read info: %+v", res)
wb.log.TRACE.Printf("read info: %+v", res)

// workaround internal NRGkick state change after connecting
// https://github.com/evcc-io/evcc/pull/274
return !res.PauseCharging || res.ChargingActive, nil
}

// Enable implements the api.Charger interface
func (nrg *NRGKickBLE) Enable(enable bool) error {
func (wb *NRGKickBLE) Enable(enable bool) error {
var res ble.Info
if err := nrg.read(ble.InfoService, &res); err != nil {
if err := wb.read(ble.InfoService, &res); err != nil {
return err
}

// workaround internal NRGkick state change after connecting
// https://github.com/evcc-io/evcc/pull/274
if !enable && res.PauseCharging {
nrg.pauseCharging = false
settings := nrg.mergeSettings(res)
wb.pauseCharging = false
settings := wb.mergeSettings(res)

nrg.log.TRACE.Printf("write settings (workaround): %+v", settings)
if err := nrg.write(ble.SettingsService, &settings); err != nil {
wb.log.TRACE.Printf("write settings (workaround): %+v", settings)
if err := wb.write(ble.SettingsService, &settings); err != nil {
return err
}
}

nrg.pauseCharging = !enable // use cached value to work around API roundtrip delay
settings := nrg.mergeSettings(res)
wb.pauseCharging = !enable // use cached value to work around API roundtrip delay
settings := wb.mergeSettings(res)

nrg.log.TRACE.Printf("write settings: %+v", settings)
wb.log.TRACE.Printf("write settings: %+v", settings)

return nrg.write(ble.SettingsService, &settings)
return wb.write(ble.SettingsService, &settings)
}

// MaxCurrent implements the api.Charger interface
func (nrg *NRGKickBLE) MaxCurrent(current int64) error {
func (wb *NRGKickBLE) MaxCurrent(current int64) error {
var res ble.Info
if err := nrg.read(ble.InfoService, &res); err != nil {
if err := wb.read(ble.InfoService, &res); err != nil {
return err
}

nrg.current = int(current) // use cached value to work around API roundtrip delay
settings := nrg.mergeSettings(res)
wb.current = int(current) // use cached value to work around API roundtrip delay
settings := wb.mergeSettings(res)

nrg.log.TRACE.Printf("write settings: %+v", settings)
wb.log.TRACE.Printf("write settings: %+v", settings)

return nrg.write(ble.SettingsService, &settings)
return wb.write(ble.SettingsService, &settings)
}

var _ api.Meter = (*NRGKickBLE)(nil)

// CurrentPower implements the api.Meter interface
func (nrg *NRGKickBLE) CurrentPower() (float64, error) {
func (wb *NRGKickBLE) CurrentPower() (float64, error) {
var res ble.Power
if err := nrg.read(ble.PowerService, &res); err != nil {
if err := wb.read(ble.PowerService, &res); err != nil {
return 0, err
}

nrg.log.TRACE.Printf("read power: %+v", res)
wb.log.TRACE.Printf("read power: %+v", res)

return float64(res.TotalPower) * 10, nil
}

var _ api.MeterEnergy = (*NRGKickBLE)(nil)

// TotalEnergy implements the api.MeterEnergy interface
func (nrg *NRGKickBLE) TotalEnergy() (float64, error) {
func (wb *NRGKickBLE) TotalEnergy() (float64, error) {
var res ble.Energy
if err := nrg.read(ble.EnergyService, &res); err != nil {
if err := wb.read(ble.EnergyService, &res); err != nil {
return 0, err
}

nrg.log.TRACE.Printf("read energy: %+v", res)
wb.log.TRACE.Printf("read energy: %+v", res)

return float64(res.TotalEnergy) / 1000, nil
}

var _ api.PhaseCurrents = (*NRGKickBLE)(nil)

// Currents implements the api.PhaseCurrents interface
func (nrg *NRGKickBLE) Currents() (float64, float64, float64, error) {
func (wb *NRGKickBLE) Currents() (float64, float64, float64, error) {
var res ble.VoltageCurrent
if err := nrg.read(ble.VoltageCurrentService, &res); err != nil {
if err := wb.read(ble.VoltageCurrentService, &res); err != nil {
return 0, 0, 0, err
}

nrg.log.TRACE.Printf("read voltage/current: %+v", res)
wb.log.TRACE.Printf("read voltage/current: %+v", res)

return float64(res.CurrentL1) / 100,
float64(res.CurrentL2) / 100,
Expand All @@ -335,11 +334,11 @@ func (nrg *NRGKickBLE) Currents() (float64, float64, float64, error) {

// ChargedEnergy implements the ChargeRater interface
// NOTE: apparently shows energy of a stopped charging session, hence substituted by TotalEnergy
// func (nrg *NRGKickBLE) ChargedEnergy() (float64, error) {
// func (wb *NRGKickBLE) ChargedEnergy() (float64, error) {
// res := ble.Energy{}
// if err := nrg.read(ble.EnergyService, &res); err != nil {
// if err := wb.read(ble.EnergyService, &res); err != nil {
// return 0, err
// }
// nrg.log.TRACE.Printf("energy: %+v", res)
// wb.log.TRACE.Printf("energy: %+v", res)
// return float64(res.EnergyLastCharge) / 1000, nil
// }
Empty file removed packaging/docker/bin/docker-btmgmt
Empty file.

0 comments on commit c3eb5f7

Please sign in to comment.