# sonar_demo.py
#
# Raspberry Pi Pico - HC-04 sonar module demo

# This module provides a class for measuring distance using an HC-04 ultrasonic
# ranger.

# The device requires one digital trigger output and one digital echo input.
# It is a 5V device, so echo needs to be level-shifted to 3.3V logic levels.  One option
# is a pair of resistors as a voltage divider.

# This implementation uses the pulseio module to measure the echo pulse width.
# But the precision appears to be low, so the result is only suitable for binary
# proximity detection.

# Likely a better long-term solution will be to use the RP2040 programmable IO
# peripheral (PIO) to precisely measure the echo duration.

################################################################
# CircuitPython module documentation:
# time       https://circuitpython.readthedocs.io/en/latest/shared-bindings/time/index.html
# board      https://circuitpython.readthedocs.io/en/latest/shared-bindings/board/index.html
# digitalio  https://circuitpython.readthedocs.io/en/latest/shared-bindings/digitalio/index.html
# pulseio    https://circuitpython.readthedocs.io/en/latest/shared-bindings/pulseio/index.html
#
# Driver lifecycle documentation:
# https://circuitpython.readthedocs.io/en/latest/docs/design_guide.html#lifetime-and-contextmanagers
#
################################################################################
# load standard Python modules
import time

# load the CircuitPython hardware definition module for pin definitions
import board

# load the CircuitPython GPIO support
import digitalio

# load the CircuitPython pulse measurement support
import pulseio

#--------------------------------------------------------------------------------
class Sonar:
    def __init__(self, ECHO=board.GP20, TRIG=board.GP21):
        """This class represents an HC-04 ultraonic ranger module.  It uses one output
        pin for trigger and one input pin for echo pulse measurement.  N.B. the
        device is 5V so the ECHO pin will need level-shifting to 3.3V logic
        levels."""

        self._trig = digitalio.DigitalInOut(TRIG)
        self._trig.direction = digitalio.Direction.OUTPUT
        self._trig.value = False

        self._echo = pulseio.PulseIn(ECHO)
        self._echo.pause()

    def ping_sync(self):
        """Measure the round-trip echo time, returning either a value in seconds or None
        if the measurement times out.  N.B. this function will not return until
        the cycle is done, so it is not compatible with asynchronous event
        loops.
        """

        self._echo.clear()
        
        # trigger the ultrasonic pulse 
        self._trig.value = True
        time.sleep(1e-5)
        self._trig.value = False

        self._echo.resume()
        start = time.monotonic_ns()
        timeout = start + int(0.5 * 1e9)

        # wait for the measurement to complete or time out
        while len(self._echo) < 1:
            now = time.monotonic_ns()
            if now > timeout:
                self._echo.pause()
                return None

        self._echo.pause()
        return self._echo[0] * 1e-6

    def deinit(self):
        """Manage resource release as part of object lifecycle."""
        self._trig.deinit()
        self._echo.deinit()
        self._trig  = None
        self._echo = None

    def __enter__(self):
        return self

    def __exit__(self):
        # Automatically deinitializes the hardware when exiting a context.
        self.deinit()

#--------------------------------------------------------------------------------
# Ultrasonic ranger demonstration.

sonar = Sonar()
print("Starting sonar test.")

while True:
    range = sonar.ping_sync()
    print(f"Range: {range} seconds round-trip time.")
    time.sleep(0.1)
