Skip to content

Commit

Permalink
gpio(hcsr04): add driver for ultrasonic ranging module
Browse files Browse the repository at this point in the history
  • Loading branch information
gen2thomas committed Oct 26, 2023
1 parent 1f09353 commit 745d602
Show file tree
Hide file tree
Showing 6 changed files with 466 additions and 4 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,19 +270,21 @@ Support for many devices that use General Purpose Input/Output (GPIO) have
a shared set of drivers provided using the `gobot/drivers/gpio` package:

- [GPIO](https://en.wikipedia.org/wiki/General_Purpose_Input/Output) <=> [Drivers](https://github.com/hybridgroup/gobot/tree/master/drivers/gpio)
- AIP1640 LED
- AIP1640 LED Dot Matrix/7 Segment Controller
- Button
- Buzzer
- Direct Pin
- EasyDriver
- Grove Button
- Grove Buzzer
- Grove LED
- Grove Magnetic Switch
- Grove Relay
- Grove Touch Sensor
- HC-SR04 Ultrasonic Ranging Module
- HD44780 LCD controller
- LED
- Makey Button
- MAX7219 LED Dot Matrix
- Motor
- Proximity Infra Red (PIR) Motion Sensor
- Relay
Expand Down
6 changes: 4 additions & 2 deletions drivers/gpio/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Please refer to the main [README.md](https://github.com/hybridgroup/gobot/blob/r

Gobot has a extensible system for connecting to hardware devices. The following GPIO devices are currently supported:

- AIP1640 LED Dot Matrix/7 Segment Controller
- Button
- Buzzer
- Direct Pin
Expand All @@ -21,14 +22,15 @@ Gobot has a extensible system for connecting to hardware devices. The following
- Grove Magnetic Switch
- Grove Relay
- Grove Touch Sensor
- HC-SR04 Ultrasonic Ranging Module
- HD44780 LCD controller
- LED
- Makey Button
- MAX7219 LED Dot Matrix
- Motor
- Proximity Infra Red (PIR) Motion Sensor
- Relay
- RGB LED
- Servo
- Stepper Motor
- TM1638 LED Controller

More drivers are coming soon...
62 changes: 62 additions & 0 deletions drivers/gpio/gpio.go → drivers/gpio/gpio_driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package gpio

import (
"errors"
"sync"

"gobot.io/x/gobot/v2"
)

var (
Expand Down Expand Up @@ -61,3 +64,62 @@ type DigitalWriter interface {
type DigitalReader interface {
DigitalRead(string) (val int, err error)
}

// Driver implements the interface gobot.Driver.
type Driver struct {
name string
connection gobot.Adaptor
afterStart func() error
beforeHalt func() error
gobot.Commander
mutex *sync.Mutex // mutex often needed to ensure that write-read sequences are not interrupted
}

// NewDriver creates a new generic and basic gpio gobot driver.
func NewDriver(a gobot.Adaptor, name string) *Driver {
d := &Driver{
name: gobot.DefaultName(name),
connection: a,
afterStart: func() error { return nil },
beforeHalt: func() error { return nil },
Commander: gobot.NewCommander(),
mutex: &sync.Mutex{},
}

return d
}

// Name returns the name of the gpio device.
func (d *Driver) Name() string {
return d.name
}

// SetName sets the name of the gpio device.
func (d *Driver) SetName(name string) {
d.name = name
}

// Connection returns the connection of the gpio device.
func (d *Driver) Connection() gobot.Connection {
return d.connection.(gobot.Connection)
}

// Start initializes the i2c device.
func (d *Driver) Start() error {
d.mutex.Lock()
defer d.mutex.Unlock()

// currently there is nothing to do here for the driver

return d.afterStart()
}

// Halt halts the i2c device.
func (d *Driver) Halt() error {
d.mutex.Lock()
defer d.mutex.Unlock()

// currently there is nothing to do after halt for the driver

return d.beforeHalt()
}
226 changes: 226 additions & 0 deletions drivers/gpio/hcsr04_driver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
package gpio

import (
"fmt"
"sync"
"time"

"gobot.io/x/gobot/v2"
"gobot.io/x/gobot/v2/system"
)

const (
hcsr04SoundSpeed = 343 // in [m/s]
// the device can measure 2 cm .. 4 m, this means sweep distances between 4 cm and 8 m
// this cause pulse duration between 0.12 ms and 24 ms (at 34.3 cm/ms, ~0.03 ms/cm, ~3 ms/m)
// so we use 60 ms as a limit for timeout and 100 ms for duration between 2 consecutive measurements
hcsr04StartTransmitTimeout time.Duration = 100 * time.Millisecond // unfortunately takes sometimes longer than 60 ms
hcsr04ReceiveTimeout time.Duration = 60 * time.Millisecond
hcsr04EmitTriggerDuration time.Duration = 10 * time.Microsecond // according to specification
hcsr04MonitorUpdate time.Duration = 200 * time.Millisecond
// the resolution of the device is ~3 mm, which relates to 10 us (343 mm/ms = 0.343 mm/us)
// the poll interval increases the reading interval to this value and adds around 3 mm inaccuracy
// it takes only an effect for fast systems, because reading inputs is typically much slower, e.g. 30-50 us on raspi
// so, using the internal edge detection with "cdev" is more precise
hcsr04PollInputIntervall time.Duration = 10 * time.Microsecond
)

// HCSR04 is a driver for ultrasonic range measurement.
type HCSR04 struct {
*Driver
triggerPinID string
echoPinID string
useEdgePolling bool // use discrete edge polling instead "cdev" from gpiod
triggerPin gobot.DigitalPinner
echoPin gobot.DigitalPinner
lastDistanceMm int // distance in mm, ~20..4000
continuousStop chan struct{}
continuousStopWaitGroup *sync.WaitGroup
measureMutex *sync.Mutex // to ensure that only one measurement is done at a time
delayMicroSecChan chan int64 // channel for event handler return value
distanceMonitorStarted bool
pollQuitChan chan struct{} // channel for quit the continuous polling
}

// NewHCSR04 creates a new instance of the driver for HC-SR04 (same as SEN-US01).
//
// Datasheet: https://www.makershop.de/download/HCSR04-datasheet-version-1.pdf
func NewHCSR04(a gobot.Adaptor, triggerPinID string, echoPinID string, useEdgePolling bool) *HCSR04 {
h := HCSR04{
Driver: NewDriver(a, "HCSR04"),
triggerPinID: triggerPinID,
echoPinID: echoPinID,
useEdgePolling: useEdgePolling,
measureMutex: &sync.Mutex{},
}

h.afterStart = func() error {
tpin, err := a.(gobot.DigitalPinnerProvider).DigitalPin(triggerPinID)
if err != nil {
return fmt.Errorf("error on get trigger pin: %v", err)
}
if err := tpin.ApplyOptions(system.WithPinDirectionOutput(0)); err != nil {
return fmt.Errorf("error on apply output for trigger pin: %v", err)
}
h.triggerPin = tpin

// pins are inputs by default
epin, err := a.(gobot.DigitalPinnerProvider).DigitalPin(echoPinID)
if err != nil {
return fmt.Errorf("error on get echo pin: %v", err)
}

epinOptions := []func(gobot.DigitalPinOptioner) bool{system.WithPinEventOnBothEdges(h.createEventHandler())}
if h.useEdgePolling {
h.pollQuitChan = make(chan struct{})
epinOptions = append(epinOptions, system.WithPinPollForEdgeDetection(hcsr04PollInputIntervall, h.pollQuitChan))
}
if err := epin.ApplyOptions(epinOptions...); err != nil {
return fmt.Errorf("error on apply options for echo pin: %v", err)
}
h.echoPin = epin

h.delayMicroSecChan = make(chan int64)

return nil
}

h.beforeHalt = func() error {
if useEdgePolling {
close(h.pollQuitChan)
}
// TODO: create summarized error
if err := h.stopDistanceMonitor(); err != nil {
fmt.Printf("no need to stop distance monitoring: %v\n", err)
}

if err := h.triggerPin.Unexport(); err != nil {
fmt.Printf("error on unexport trigger pin: %v\n", err)
}

if err := h.echoPin.Unexport(); err != nil {
fmt.Printf("error on unexport echo pin: %v\n", err)
}

close(h.delayMicroSecChan)

return nil
}

return &h
}

// MeasureDistance retrieves the distance in front of sensor in meters and returns the measure. It is not designed
// to work in a fast loop! For this specific usage, use StartDistanceMonitor() associated with GetDistance() instead.
func (h *HCSR04) MeasureDistance() (float64, error) {
err := h.measureDistance()
if err != nil {
return 0, err
}
return h.GetDistance(), nil
}

// StartDistanceMonitor starts continuous measurement. The current value can be read by GetDistance()
func (h *HCSR04) StartDistanceMonitor() error {
// ensure that start and stop can not interfere
h.mutex.Lock()
defer h.mutex.Unlock()

if h.distanceMonitorStarted {
return fmt.Errorf("distance monitor already started for '%s'", h.name)
}

h.distanceMonitorStarted = true
h.continuousStop = make(chan struct{})
h.continuousStopWaitGroup = &sync.WaitGroup{}
h.continuousStopWaitGroup.Add(1)

go func(name string) {
defer h.continuousStopWaitGroup.Done()
for {
select {
case <-h.continuousStop:
return
default:
if err := h.measureDistance(); err != nil {
fmt.Printf("continuous measure distance skipped for '%s': %v\n", name, err)
}
time.Sleep(hcsr04MonitorUpdate)
}
}
}(h.name)

return nil
}

// StopDistanceMonitor stop the monitor process
func (h *HCSR04) StopDistanceMonitor() error {
// ensure that start and stop can not interfere
h.mutex.Lock()
defer h.mutex.Unlock()

return h.stopDistanceMonitor()
}

func (h *HCSR04) stopDistanceMonitor() error {
if !h.distanceMonitorStarted {
return fmt.Errorf("distance monitor is not yet started for '%s'", h.name)
}

h.continuousStop <- struct{}{}
h.continuousStopWaitGroup.Wait()
h.distanceMonitorStarted = false

return nil
}

// GetDistance returns the last distance measured in meter, it does not trigger a distance measurement
func (h *HCSR04) GetDistance() float64 {
return float64(h.lastDistanceMm) / 1000.0
}

func (h *HCSR04) createEventHandler() func(int, time.Duration, string, uint32, uint32) {
var startTimestamp time.Duration
return func(offset int, t time.Duration, et string, sn uint32, lsn uint32) {
switch et {
case system.DigitalPinEventRisingEdge:
startTimestamp = t
case system.DigitalPinEventFallingEdge:
// unfortunately there is an additional falling edge at each start trigger, so we need to filter this
// we use the start duration value for filtering
if startTimestamp == 0 {
return
}
h.delayMicroSecChan <- (t - startTimestamp).Microseconds()
startTimestamp = 0
}
}
}

func (h *HCSR04) measureDistance() error {
h.measureMutex.Lock()
defer h.measureMutex.Unlock()

if err := h.emitTrigger(); err != nil {
return err
}

// stop the loop if the measure is done or the timeout is elapsed
timeout := hcsr04StartTransmitTimeout + hcsr04ReceiveTimeout
select {
case <-time.After(timeout):
return fmt.Errorf("timeout %s reached while waiting for value with echo pin %s", timeout, h.echoPinID)
case durMicro := <-h.delayMicroSecChan:
h.lastDistanceMm = int(durMicro * hcsr04SoundSpeed / 1000 / 2)
}

return nil
}

func (h *HCSR04) emitTrigger() error {
if err := h.triggerPin.Write(1); err != nil {
return err
}
time.Sleep(hcsr04EmitTriggerDuration)
return h.triggerPin.Write(0)
}
Loading

0 comments on commit 745d602

Please sign in to comment.