hybridgroup.gobot/drivers/gpio/pir_motion_driver.go

161 lines
4.0 KiB
Go

package gpio
import (
"fmt"
"time"
"gobot.io/x/gobot/v2"
)
// pirMotionOptionApplier needs to be implemented by each configurable option type
type pirMotionOptionApplier interface {
apply(cfg *pirMotionConfiguration)
}
// pirMotionConfiguration contains all changeable attributes of the driver.
type pirMotionConfiguration struct {
readInterval time.Duration
}
// pirMotionReadIntervalOption is the type for applying another read interval to the configuration
type pirMotionReadIntervalOption time.Duration
// PIRMotionDriver represents a digital Proximity Infra Red (PIR) motion detecter
//
// Supported options:
//
// "WithName"
type PIRMotionDriver struct {
*driver
pirMotionCfg *pirMotionConfiguration
gobot.Eventer
active bool
halt chan struct{}
}
// NewPIRMotionDriver returns a new driver for PIR motion sensor with a polling interval of 10 Milliseconds,
// given a DigitalReader and pin.
//
// Supported options:
//
// "WithName"
// "WithButtonPollInterval"
func NewPIRMotionDriver(a DigitalReader, pin string, opts ...interface{}) *PIRMotionDriver {
//nolint:forcetypeassert // no error return value, so there is no better way
d := &PIRMotionDriver{
driver: newDriver(a.(gobot.Connection), "PIRMotion", withPin(pin)),
pirMotionCfg: &pirMotionConfiguration{readInterval: 10 * time.Millisecond},
}
d.afterStart = d.initialize
d.beforeHalt = d.shutdown
for _, opt := range opts {
switch o := opt.(type) {
case optionApplier:
o.apply(d.driverCfg)
case pirMotionOptionApplier:
o.apply(d.pirMotionCfg)
case time.Duration:
// TODO this is only for backward compatibility and will be removed after version 2.x
d.pirMotionCfg.readInterval = o
default:
panic(fmt.Sprintf("'%s' can not be applied on '%s'", opt, d.driverCfg.name))
}
}
return d
}
// WithPIRMotionPollInterval change the asynchronous cyclic reading interval from default 10ms to the given value.
func WithPIRMotionPollInterval(interval time.Duration) pirMotionOptionApplier {
return pirMotionReadIntervalOption(interval)
}
// Active gets the current state
func (d *PIRMotionDriver) Active() bool {
// ensure that read and write can not interfere
d.mutex.Lock()
defer d.mutex.Unlock()
return d.active
}
// initialize the PIRMotionDriver and polls the state of the sensor at the given interval.
//
// Emits the Events:
//
// MotionDetected - On motion detected
// MotionStopped int - On motion stopped
// Error error - On pirMotion error
//
// The PIRMotionDriver will send the MotionDetected event over and over,
// 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 (d *PIRMotionDriver) initialize() error {
if d.pirMotionCfg.readInterval == 0 {
return fmt.Errorf("the read interval for pirMotion needs to be greater than zero")
}
d.Eventer = gobot.NewEventer()
d.AddEvent(MotionDetected)
d.AddEvent(MotionStopped)
d.AddEvent(Error)
d.halt = make(chan struct{})
go func() {
for {
select {
case <-time.After(d.pirMotionCfg.readInterval):
newValue, err := d.digitalRead(d.driverCfg.pin)
if err != nil {
d.Publish(Error, err)
}
d.update(newValue)
case <-d.halt:
return
}
}
}()
return nil
}
// shutdown stops polling
func (d *PIRMotionDriver) shutdown() error {
if d.pirMotionCfg.readInterval == 0 || d.halt == nil {
// cyclic reading deactivated
return nil
}
close(d.halt) // broadcast halt, also to the test
return nil
}
func (d *PIRMotionDriver) update(newValue int) {
// ensure that read and write can not interfere
d.mutex.Lock()
defer d.mutex.Unlock()
switch newValue {
case 1:
if !d.active {
d.active = true
d.Publish(MotionDetected, newValue)
}
case 0:
if d.active {
d.active = false
d.Publish(MotionStopped, newValue)
}
}
}
func (o pirMotionReadIntervalOption) String() string {
return "read interval option for PIR motion sensor"
}
func (o pirMotionReadIntervalOption) apply(cfg *pirMotionConfiguration) {
cfg.readInterval = time.Duration(o)
}