Skip to content

Commit

Permalink
gpio: fix data race in PIRMotionDriver (#1028)
Browse files Browse the repository at this point in the history
  • Loading branch information
gen2thomas authored Nov 1, 2023
1 parent c41604f commit 9e311b2
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 88 deletions.
6 changes: 3 additions & 3 deletions drivers/gpio/button_driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func (b *ButtonDriver) SetDefaultState(s int) {
// Push int - On button push
// Release int - On button release
// Error error - On button error
func (b *ButtonDriver) initialize() (err error) {
func (b *ButtonDriver) initialize() error {
state := b.defaultState
go func() {
for {
Expand All @@ -93,10 +93,10 @@ func (b *ButtonDriver) initialize() (err error) {
}
}
}()
return
return nil
}

func (b *ButtonDriver) shutdown() (err error) {
func (b *ButtonDriver) shutdown() error {
b.halt <- true
return nil
}
Expand Down
8 changes: 5 additions & 3 deletions drivers/gpio/button_driver_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package gpio

import (
"errors"
"fmt"
"strings"
"sync"
"testing"
Expand Down Expand Up @@ -58,7 +58,7 @@ func TestButtonStart(t *testing.T) {
select {
case val = <-nextVal:
if val < 0 {
err = errors.New("digital read error")
err = fmt.Errorf("digital read error")
}
return val, err
default:
Expand All @@ -73,9 +73,11 @@ func TestButtonStart(t *testing.T) {
})

// act
assert.NoError(t, d.Start())
err := d.Start()

// assert & rearrange
assert.NoError(t, err)

select {
case <-sem:
case <-time.After(buttonTestDelay * time.Millisecond):
Expand Down
93 changes: 50 additions & 43 deletions drivers/gpio/pir_motion_driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,12 @@ import (

// PIRMotionDriver represents a digital Proximity Infra Red (PIR) motion detecter
type PIRMotionDriver struct {
Active bool
pin string
name string
halt chan bool
interval time.Duration
connection DigitalReader
*Driver
gobot.Eventer
pin string
active bool
halt chan bool
interval time.Duration
}

// NewPIRMotionDriver returns a new PIRMotionDriver with a polling interval of
Expand All @@ -25,14 +24,15 @@ type PIRMotionDriver struct {
// time.Duration: Interval at which the PIRMotionDriver is polled for new information
func NewPIRMotionDriver(a DigitalReader, pin string, v ...time.Duration) *PIRMotionDriver {
b := &PIRMotionDriver{
name: gobot.DefaultName("PIRMotion"),
connection: a,
pin: pin,
Active: false,
Eventer: gobot.NewEventer(),
interval: 10 * time.Millisecond,
halt: make(chan bool),
Driver: NewDriver(a.(gobot.Connection), "PIRMotion"),
Eventer: gobot.NewEventer(),
pin: pin,
active: false,
interval: 10 * time.Millisecond,
halt: make(chan bool),
}
b.afterStart = b.initialize
b.beforeHalt = b.shutdown

if len(v) > 0 {
b.interval = v[0]
Expand All @@ -45,7 +45,19 @@ func NewPIRMotionDriver(a DigitalReader, pin string, v ...time.Duration) *PIRMot
return b
}

// Start starts the PIRMotionDriver and polls the state of the sensor at the given interval.
// Pin returns the PIRMotionDriver pin
func (p *PIRMotionDriver) Pin() string { return p.pin }

// Active gets the current state
func (p *PIRMotionDriver) Active() bool {
// ensure that read and write can not interfere
p.mutex.Lock()
defer p.mutex.Unlock()

return p.active
}

// initialize the PIRMotionDriver and polls the state of the sensor at the given interval.
//
// Emits the Events:
//
Expand All @@ -57,50 +69,45 @@ func NewPIRMotionDriver(a DigitalReader, pin string, v ...time.Duration) *PIRMot
// just as long as motion is still being detected.
// It will only send the MotionStopped event once, however, until
// motion starts being detected again
func (p *PIRMotionDriver) Start() (err error) {
func (p *PIRMotionDriver) initialize() error {
go func() {
for {
newValue, err := p.connection.DigitalRead(p.Pin())
newValue, err := p.connection.(DigitalReader).DigitalRead(p.Pin())
if err != nil {
p.Publish(Error, err)
}
switch newValue {
case 1:
if !p.Active {
p.Active = true
p.Publish(MotionDetected, newValue)
}
case 0:
if p.Active {
p.Active = false
p.Publish(MotionStopped, newValue)
}
}

p.update(newValue)
select {
case <-time.After(p.interval):
case <-p.halt:
return
}
}
}()
return
return nil
}

// Halt stops polling the button for new information
func (p *PIRMotionDriver) Halt() (err error) {
// shutdown stops polling
func (p *PIRMotionDriver) shutdown() error {
p.halt <- true
return
return nil
}

// Name returns the PIRMotionDriver name
func (p *PIRMotionDriver) Name() string { return p.name }
func (p *PIRMotionDriver) update(newValue int) {
// ensure that read and write can not interfere
p.mutex.Lock()
defer p.mutex.Unlock()

// SetName sets the PIRMotionDriver name
func (p *PIRMotionDriver) SetName(n string) { p.name = n }

// Pin returns the PIRMotionDriver pin
func (p *PIRMotionDriver) Pin() string { return p.pin }

// Connection returns the PIRMotionDriver Connection
func (p *PIRMotionDriver) Connection() gobot.Connection { return p.connection.(gobot.Connection) }
switch newValue {
case 1:
if !p.active {
p.active = true
p.Publish(MotionDetected, newValue)
}
case 0:
if p.active {
p.active = false
p.Publish(MotionStopped, newValue)
}
}
}
151 changes: 112 additions & 39 deletions drivers/gpio/pir_motion_driver_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package gpio

import (
"errors"
"fmt"
"strings"
"sync"
"testing"
"time"

Expand All @@ -14,42 +15,67 @@ var _ gobot.Driver = (*PIRMotionDriver)(nil)

const motionTestDelay = 150

func initTestPIRMotionDriver() *PIRMotionDriver {
return NewPIRMotionDriver(newGpioTestAdaptor(), "1")
}

func TestPIRMotionDriverHalt(t *testing.T) {
d := initTestPIRMotionDriver()
go func() {
<-d.halt
}()
assert.NoError(t, d.Halt())
func initTestPIRMotionDriverWithStubbedAdaptor() (*PIRMotionDriver, *gpioTestAdaptor) {
a := newGpioTestAdaptor()
d := NewPIRMotionDriver(a, "1")
return d, a
}

func TestPIRMotionDriver(t *testing.T) {
d := NewPIRMotionDriver(newGpioTestAdaptor(), "1")
assert.NotNil(t, d.Connection())

func TestNewPIRMotionDriver(t *testing.T) {
// arrange
a := newGpioTestAdaptor()
// act
d := NewPIRMotionDriver(a, "1")
// assert
assert.IsType(t, &PIRMotionDriver{}, d)
assert.True(t, strings.HasPrefix(d.name, "PIRMotion"))
assert.Equal(t, a, d.connection)
assert.NotNil(t, d.afterStart)
assert.NotNil(t, d.beforeHalt)
assert.NotNil(t, d.Commander)
assert.NotNil(t, d.mutex)
assert.NotNil(t, d.Eventer)
assert.Equal(t, "1", d.pin)
assert.Equal(t, false, d.active)
assert.Equal(t, 10*time.Millisecond, d.interval)
assert.NotNil(t, d.halt)
// act & assert other interval
d = NewPIRMotionDriver(newGpioTestAdaptor(), "1", 30*time.Second)
assert.Equal(t, 30*time.Second, d.interval)
}

func TestPIRMotionDriverStart(t *testing.T) {
func TestPIRMotionStart(t *testing.T) {
// arrange
sem := make(chan bool)
nextVal := make(chan int, 1)
a := newGpioTestAdaptor()
d := NewPIRMotionDriver(a, "1")

assert.NoError(t, d.Start())
a.digitalReadFunc = func(string) (int, error) {
val := 1
var err error
select {
case val = <-nextVal:
if val < 0 {
err = fmt.Errorf("digital read error")
}
return val, err
default:
return val, err
}
}

_ = d.Once(MotionDetected, func(data interface{}) {
assert.True(t, d.Active)
assert.True(t, d.active)
nextVal <- 0
sem <- true
})

a.digitalReadFunc = func(string) (val int, err error) {
val = 1
return
}
// act
err := d.Start()

// assert & rearrange
assert.NoError(t, err)

select {
case <-sem:
Expand All @@ -58,15 +84,11 @@ func TestPIRMotionDriverStart(t *testing.T) {
}

_ = d.Once(MotionStopped, func(data interface{}) {
assert.False(t, d.Active)
assert.False(t, d.active)
nextVal <- -1
sem <- true
})

a.digitalReadFunc = func(string) (val int, err error) {
val = 0
return
}

select {
case <-sem:
case <-time.After(motionTestDelay * time.Millisecond):
Expand All @@ -77,25 +99,76 @@ func TestPIRMotionDriverStart(t *testing.T) {
sem <- true
})

a.digitalReadFunc = func(string) (val int, err error) {
err = errors.New("digital read error")
return
select {
case <-sem:
case <-time.After(motionTestDelay * time.Millisecond):
t.Errorf("PIRMotionDriver Event \"Error\" was not published")
}

_ = d.Once(MotionDetected, func(data interface{}) {
sem <- true
})

d.halt <- true
nextVal <- 1

select {
case <-sem:
t.Errorf("PIRMotion Event \"MotionDetected\" should not published")
case <-time.After(motionTestDelay * time.Millisecond):
t.Errorf("PIRMotionDriver Event \"Error\" was not published")
}
}

func TestPIRDriverDefaultName(t *testing.T) {
d := initTestPIRMotionDriver()
assert.True(t, strings.HasPrefix(d.Name(), "PIR"))
func TestPIRMotionHalt(t *testing.T) {
// arrange
d, _ := initTestPIRMotionDriverWithStubbedAdaptor()
const timeout = 10 * time.Microsecond
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
select {
case <-d.halt: // wait until halt was set to the channel
case <-time.After(timeout): // otherwise run into the timeout
t.Errorf("halt was not received within %s", timeout)
}
}()
// act & assert
assert.NoError(t, d.Halt())
wg.Wait() // wait until the go function was really finished
}

func TestPIRMotionPin(t *testing.T) {
tests := map[string]struct {
want string
}{
"10": {want: "10"},
"36": {want: "36"},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
// arrange
d := PIRMotionDriver{pin: name}
// act & assert
assert.Equal(t, tc.want, d.Pin())
})
}
}

func TestPIRDriverSetName(t *testing.T) {
d := initTestPIRMotionDriver()
d.SetName("mybot")
assert.Equal(t, "mybot", d.Name())
func TestPIRMotionActive(t *testing.T) {
tests := map[string]struct {
want bool
}{
"active_true": {want: true},
"active_false": {want: false},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
// arrange
d := PIRMotionDriver{Driver: NewDriver(nil, "PIRMotion")} // just for mutex
d.active = tc.want
// act & assert
assert.Equal(t, tc.want, d.Active())
})
}
}

0 comments on commit 9e311b2

Please # to comment.