# Source code for gpiozero.pins.pigpio

    # vim: set fileencoding=utf-8:
    #
    # GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins
    #
    # Copyright (c) 2021 Kyle Morgan <kyle@knmorgan.net>
    # Copyright (c) 2016-2021 Dave Jones <dave@waveform.org.uk>
    # Copyright (c) 2020 Ben Nuttall <ben@bennuttall.com>
    # Copyright (c) 2019 Maksim Levental <maksim.levental@gmail.com>
    # Copyright (c) 2019 Aaron Rogers <aaron.kyle.rogers@gmail.com>
    # Copyright (c) 2016 BuildTools <david.glaude@gmail.com>
    # Copyright (c) 2016 Andrew Scheller <github@loowis.durge.org>
    #
    # SPDX-License-Identifier: BSD-3-Clause

    from __future__ import (
        unicode_literals,
        absolute_import,
        print_function,
        division,
        )
    str = type('')

    import os

    import pigpio

    from . import SPI
    from .pi import PiPin, PiFactory, spi_port_device
    from ..mixins import SharedMixin
    from ..exc import (
        PinInvalidFunction,
        PinSetInput,
        PinFixedPull,
        PinInvalidPull,
        PinInvalidBounce,
        PinInvalidState,
        SPIBadArgs,
        SPIInvalidClockMode,
        PinPWMFixedValue,
        DeviceClosed
    )

    class PiGPIOFactory(PiFactory):
        """
        Extends :class:`~gpiozero.pins.pi.PiFactory`. Uses the `pigpio`_ library to
        interface to the Pi's GPIO pins. The pigpio library relies on a daemon
        (:command:`pigpiod`) to be running as root to provide access to the GPIO
        pins, and communicates with this daemon over a network socket.

        While this does mean only the daemon itself should control the pins, the
        architecture does have several advantages:

        * Pins can be remote controlled from another machine (the other
          machine doesn't even have to be a Raspberry Pi; it simply needs the
          `pigpio`_ client library installed on it)
        * The daemon supports hardware PWM via the DMA controller
        * Your script itself doesn't require root privileges; it just needs to
          be able to communicate with the daemon

        You can construct pigpio pins manually like so::

            from gpiozero.pins.pigpio import PiGPIOFactory
            from gpiozero import LED

            factory = PiGPIOFactory()
            led = LED(12, pin_factory=factory)

        This is particularly useful for controlling pins on a remote machine. To
        accomplish this simply specify the host (and optionally port) when
        constructing the pin::

            from gpiozero.pins.pigpio import PiGPIOFactory
            from gpiozero import LED

            factory = PiGPIOFactory(host='192.168.0.2')
            led = LED(12, pin_factory=factory)

        .. note::

            In some circumstances, especially when playing with PWM, it does appear
            to be possible to get the daemon into "unusual" states. We would be
            most interested to hear any bug reports relating to this (it may be a
            bug in our pin implementation). A workaround for now is simply to
            restart the :command:`pigpiod` daemon.

        .. _pigpio: http://abyz.me.uk/rpi/pigpio/
        """
        def __init__(self, host=None, port=None):
            super(PiGPIOFactory, self).__init__()
            if host is None:
                host = os.environ.get('PIGPIO_ADDR', 'localhost')
            if port is None:
                # XXX Use getservbyname
                port = int(os.environ.get('PIGPIO_PORT', 8888))
            self.pin_class = PiGPIOPin
            self._connection = pigpio.pi(host, port)
            # Annoyingly, pigpio doesn't raise an exception when it fails to make a
            # connection; it returns a valid (but disconnected) pi object
            if self.connection is None:
                raise IOError('failed to connect to %s:%s' % (host, port))
            self._host = host
            self._port = port
            self._spis = []

        def close(self):
            super(PiGPIOFactory, self).close()
            # We *have* to keep track of SPI interfaces constructed with pigpio;
            # if we fail to close them they prevent future interfaces from using
            # the same pins
            if self.connection:
                while self._spis:
                    self._spis[0].close()
                self.connection.stop()
                self._connection = None

        @property
        def connection(self):
            # If we're shutting down, the connection may have disconnected itself
            # already. Unfortunately, the connection's "connected" property is
            # rather buggy - disconnecting doesn't set it to False! So we're
            # naughty and check an internal variable instead...
            try:
                if self._connection.sl.s is not None:
                    return self._connection
            except AttributeError:
                pass

        @property
        def host(self):
            return self._host

        @property
        def port(self):
            return self._port

        def _get_revision(self):
            return self.connection.get_hardware_revision()

        def _get_spi_class(self, shared, hardware):
            return {
                (False, True):  PiGPIOHardwareSPI,
                (True,  True):  PiGPIOHardwareSPIShared,
                (False, False): PiGPIOSoftwareSPI,
                (True,  False): PiGPIOSoftwareSPIShared,
                }[shared, hardware]

        def spi(self, **spi_args):
            intf = super(PiGPIOFactory, self).spi(**spi_args)
            self._spis.append(intf)
            return intf

        def ticks(self):
            return self._connection.get_current_tick()

        @staticmethod
        def ticks_diff(later, earlier):
            # NOTE: pigpio ticks are unsigned 32-bit quantities that wrap every
            # 71.6 minutes. The modulo below (oh the joys of having an *actual*
            # modulo operator, unlike C's remainder) ensures the result is valid
            # even when later < earlier due to wrap-around (assuming the duration
            # measured is not longer than the period)
            return ((later - earlier) % 0x100000000) / 1000000

    class PiGPIOPin(PiPin):
        """
        Extends :class:`~gpiozero.pins.pi.PiPin`. Pin implementation for the
        `pigpio`_ library. See :class:`PiGPIOFactory` for more information.

        .. _pigpio: http://abyz.me.uk/rpi/pigpio/
        """
        GPIO_FUNCTIONS = {
            'input':   pigpio.INPUT,
            'output':  pigpio.OUTPUT,
            'alt0':    pigpio.ALT0,
            'alt1':    pigpio.ALT1,
            'alt2':    pigpio.ALT2,
            'alt3':    pigpio.ALT3,
            'alt4':    pigpio.ALT4,
            'alt5':    pigpio.ALT5,
            }

        GPIO_PULL_UPS = {
            'up':       pigpio.PUD_UP,
            'down':     pigpio.PUD_DOWN,
            'floating': pigpio.PUD_OFF,
            }

        GPIO_EDGES = {
            'both':    pigpio.EITHER_EDGE,
            'rising':  pigpio.RISING_EDGE,
            'falling': pigpio.FALLING_EDGE,
            }

        GPIO_FUNCTION_NAMES = {v: k for (k, v) in GPIO_FUNCTIONS.items()}
        GPIO_PULL_UP_NAMES = {v: k for (k, v) in GPIO_PULL_UPS.items()}
        GPIO_EDGES_NAMES = {v: k for (k, v) in GPIO_EDGES.items()}

        def __init__(self, factory, number):
            super(PiGPIOPin, self).__init__(factory, number)
            self._pull = 'up' if self.factory.pi_info.pulled_up(repr(self)) else 'floating'
            self._pwm = False
            self._bounce = None
            self._callback = None
            self._edges = pigpio.EITHER_EDGE
            try:
                self.factory.connection.set_mode(self.number, pigpio.INPUT)
            except pigpio.error as e:
                raise ValueError(e)
            self.factory.connection.set_pull_up_down(self.number, self.GPIO_PULL_UPS[self._pull])
            self.factory.connection.set_glitch_filter(self.number, 0)

        def close(self):
            if self.factory.connection:
                self.frequency = None
                self.when_changed = None
                self.function = 'input'
                self.pull = 'up' if self.factory.pi_info.pulled_up(repr(self)) else 'floating'

        def _get_function(self):
            return self.GPIO_FUNCTION_NAMES[self.factory.connection.get_mode(self.number)]

        def _set_function(self, value):
            if value != 'input':
                self._pull = 'floating'
            try:
                self.factory.connection.set_mode(self.number, self.GPIO_FUNCTIONS[value])
            except KeyError:
                raise PinInvalidFunction('invalid function "%s" for pin %r' % (value, self))

        def _get_state(self):
            if self._pwm:
                return (
                    self.factory.connection.get_PWM_dutycycle(self.number) /
                    self.factory.connection.get_PWM_range(self.number)
                    )
            else:
                return bool(self.factory.connection.read(self.number))

        def _set_state(self, value):
            if self._pwm:
                try:
                    value = int(value * self.factory.connection.get_PWM_range(self.number))
                    if value != self.factory.connection.get_PWM_dutycycle(self.number):
                        self.factory.connection.set_PWM_dutycycle(self.number, value)
                except pigpio.error:
                    raise PinInvalidState('invalid state "%s" for pin %r' % (value, self))
            elif self.function == 'input':
                raise PinSetInput('cannot set state of pin %r' % self)
            else:
                # write forces pin to OUTPUT, hence the check above
                self.factory.connection.write(self.number, bool(value))

        def _get_pull(self):
            return self._pull

        def _set_pull(self, value):
            if self.function != 'input':
                raise PinFixedPull('cannot set pull on non-input pin %r' % self)
            if value != 'up' and self.factory.pi_info.pulled_up(repr(self)):
                raise PinFixedPull('%r has a physical pull-up resistor' % self)
            try:
                self.factory.connection.set_pull_up_down(self.number, self.GPIO_PULL_UPS[value])
                self._pull = value
            except KeyError:
                raise PinInvalidPull('invalid pull "%s" for pin %r' % (value, self))

        def _get_frequency(self):
            if self._pwm:
                return self.factory.connection.get_PWM_frequency(self.number)
            return None

        def _set_frequency(self, value):
            if not self._pwm and value is not None:
                if self.function != 'output':
                    raise PinPWMFixedValue('cannot start PWM on pin %r' % self)
                # NOTE: the pin's state *must* be set to zero; if it's currently
                # high, starting PWM and setting a 0 duty-cycle *doesn't* bring
                # the pin low; it stays high!
                self.factory.connection.write(self.number, 0)
                self.factory.connection.set_PWM_frequency(self.number, int(value))
                self.factory.connection.set_PWM_range(self.number, 10000)
                self.factory.connection.set_PWM_dutycycle(self.number, 0)
                self._pwm = True
            elif self._pwm and value is not None:
                if value != self.factory.connection.get_PWM_frequency(self.number):
                    self.factory.connection.set_PWM_frequency(self.number, int(value))
                    self.factory.connection.set_PWM_range(self.number, 10000)
            elif self._pwm and value is None:
                self.factory.connection.write(self.number, 0)
                self._pwm = False

        def _get_bounce(self):
            return None if not self._bounce else self._bounce / 1000000

        def _set_bounce(self, value):
            if value is None:
                value = 0
            elif not 0 <= value <= 0.3:
                raise PinInvalidBounce('bounce must be between 0 and 0.3')
            self.factory.connection.set_glitch_filter(self.number, int(value * 1000000))

        def _get_edges(self):
            return self.GPIO_EDGES_NAMES[self._edges]

        def _set_edges(self, value):
            f = self.when_changed
            self.when_changed = None
            try:
                self._edges = self.GPIO_EDGES[value]
            finally:
                self.when_changed = f

        def _call_when_changed(self, gpio, level, ticks):
            super(PiGPIOPin, self)._call_when_changed(ticks, level)

        def _enable_event_detect(self):
            self._callback = self.factory.connection.callback(
                    self.number, self._edges, self._call_when_changed)

        def _disable_event_detect(self):
            if self._callback is not None:
                self._callback.cancel()
                self._callback = None

    class PiGPIOHardwareSPI(SPI):
        """
        Hardware SPI implementation for the `pigpio`_ library. Uses the ``spi_*``
        functions from the pigpio API.

        .. _pigpio: http://abyz.me.uk/rpi/pigpio/
        """
        def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin, pin_factory):
            port, device = spi_port_device(
                clock_pin, mosi_pin, miso_pin, select_pin)
            self._port = port
            self._device = device
            self._handle = None
            super(PiGPIOHardwareSPI, self).__init__(pin_factory=pin_factory)
            to_reserve = {clock_pin, select_pin}
            if mosi_pin is not None:
                to_reserve.add(mosi_pin)
            if miso_pin is not None:
                to_reserve.add(miso_pin)
            self.pin_factory.reserve_pins(self, *to_reserve)
            self._spi_flags = (8 << 16) | (port << 8)
            self._baud = 500000
            self._handle = self.pin_factory.connection.spi_open(
                device, self._baud, self._spi_flags)

        def _conflicts_with(self, other):
            return not (
                isinstance(other, PiGPIOHardwareSPI) and
                (self.pin_factory.host, self._port, self._device) !=
                (other.pin_factory.host, other._port, other._device)
                )

        def close(self):
            try:
                self.pin_factory._spis.remove(self)
            except (ReferenceError, ValueError):
                # If the factory has died already or we're not present in its
                # internal list, ignore the error
                pass
            if not self.closed:
                self.pin_factory.connection.spi_close(self._handle)
            self._handle = None
            self.pin_factory.release_all(self)
            super(PiGPIOHardwareSPI, self).close()

        @property
        def closed(self):
            return self._handle is None or self.pin_factory.connection is None

        def __repr__(self):
            try:
                self._check_open()
                return 'SPI(port=%d, device=%d)' % (self._port, self._device)
            except DeviceClosed:
                return 'SPI(closed)'

        def _get_clock_mode(self):
            return self._spi_flags & 0x3

        def _set_clock_mode(self, value):
            self._check_open()
            if not 0 <= value < 4:
                raise SPIInvalidClockMode("%d is not a valid SPI clock mode" % value)
            self.pin_factory.connection.spi_close(self._handle)
            self._spi_flags = (self._spi_flags & ~0x3) | value
            self._handle = self.pin_factory.connection.spi_open(
                self._device, self._baud, self._spi_flags)

        def _get_select_high(self):
            return bool((self._spi_flags >> (2 + self._device)) & 0x1)

        def _set_select_high(self, value):
            self._check_open()
            self.pin_factory.connection.spi_close(self._handle)
            self._spi_flags = (self._spi_flags & ~0x1c) | (bool(value) << (2 + self._device))
            self._handle = self.pin_factory.connection.spi_open(
                self._device, self._baud, self._spi_flags)

        def _get_bits_per_word(self):
            return (self._spi_flags >> 16) & 0x3f

        def _set_bits_per_word(self, value):
            self._check_open()
            self.pin_factory.connection.spi_close(self._handle)
            self._spi_flags = (self._spi_flags & ~0x3f0000) | ((value & 0x3f) << 16)
            self._handle = self.pin_factory.connection.spi_open(
                self._device, self._baud, self._spi_flags)

        def _get_rate(self):
            return self._baud

        def _set_rate(self, value):
            self._check_open()
            value = int(value)
            self.pin_factory.connection.spi_close(self._handle)
            self._baud = value
            self._handle = self.pin_factory.connection.spi_open(
                self._device, self._baud, self._spi_flags)

        def _get_lsb_first(self):
            return bool((self._spi_flags >> 14) & 0x1) if self._port else False

        def _set_lsb_first(self, value):
            if self._port:
                self._check_open()
                self.pin_factory.connection.spi_close(self._handle)
                self._spi_flags = (
                    (self._spi_flags & ~0xc000)
                    | (bool(value) << 14)
                    | (bool(value) << 15)
                    )
                self._handle = self.pin_factory.connection.spi_open(
                    self._device, self._baud, self._spi_flags)
            else:
                super(PiGPIOHardwareSPI, self)._set_lsb_first(value)

        def transfer(self, data):
            self._check_open()
            count, data = self.pin_factory.connection.spi_xfer(self._handle, data)
            if count < 0:
                raise IOError('SPI transfer error %d' % count)
            # Convert returned bytearray to list of ints. XXX Not sure how non-byte
            # sized words (aux intf only) are returned ... padded to 16/32-bits?
            return [int(b) for b in data]

    class PiGPIOSoftwareSPI(SPI):
        """
        Software SPI implementation for the `pigpio`_ library. Uses the ``bb_spi_*``
        functions from the pigpio API.

        .. _pigpio: http://abyz.me.uk/rpi/pigpio/
        """
        def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin, pin_factory):
            self._closed = True
            self._select_pin = select_pin
            self._clock_pin = clock_pin
            self._mosi_pin = mosi_pin
            self._miso_pin = miso_pin
            super(PiGPIOSoftwareSPI, self).__init__(pin_factory=pin_factory)
            # Can't "unreserve" MOSI/MISO on this implementation
            self.pin_factory.reserve_pins(
                self,
                clock_pin,
                mosi_pin,
                miso_pin,
                select_pin,
                )
            self._spi_flags = 0
            self._baud = 100000
            try:
                self.pin_factory.connection.bb_spi_open(
                    select_pin, miso_pin, mosi_pin, clock_pin,
                    self._baud, self._spi_flags)
                # Only set after opening bb_spi; if that fails then close() will
                # also fail if bb_spi_close is attempted on an un-open interface
                self._closed = False
            except:
                self.close()
                raise

        def _conflicts_with(self, other):
            return not (
                isinstance(other, PiGPIOSoftwareSPI) and
                (self._select_pin) != (other._select_pin)
                )

        def close(self):
            try:
                self.pin_factory._spis.remove(self)
            except (ReferenceError, ValueError):
                # If the factory has died already or we're not present in its
                # internal list, ignore the error
                pass
            if not self._closed and self.pin_factory.connection:
                self._closed = True
                self.pin_factory.connection.bb_spi_close(self._select_pin)
            self.pin_factory.release_all(self)
            super(PiGPIOSoftwareSPI, self).close()

        @property
        def closed(self):
            return self._closed

        def __repr__(self):
            try:
                self._check_open()
                return (
                    'SPI(clock_pin=%d, mosi_pin=%d, miso_pin=%d, select_pin=%d)' % (
                    self._clock_pin, self._mosi_pin, self._miso_pin, self._select_pin
                    ))
            except DeviceClosed:
                return 'SPI(closed)'

        def _spi_flags(self):
            return (
                self._mode          << 0  |
                self._select_high   << 2  |
                self._lsb_first     << 14 |
                self._lsb_first     << 15
                )

        def _get_clock_mode(self):
            return self._spi_flags & 0x3

        def _set_clock_mode(self, value):
            self._check_open()
            if not 0 <= value < 4:
                raise SPIInvalidClockMode("%d is not a valid SPI clock mode" % value)
            self.pin_factory.connection.bb_spi_close(self._select_pin)
            self._spi_flags = (self._spi_flags & ~0x3) | value
            self.pin_factory.connection.bb_spi_open(
                self._select_pin, self._miso_pin, self._mosi_pin, self._clock_pin,
                self._baud, self._spi_flags)

        def _get_select_high(self):
            return bool(self._spi_flags & 0x4)

        def _set_select_high(self, value):
            self._check_open()
            self.pin_factory.connection.bb_spi_close(self._select_pin)
            self._spi_flags = (self._spi_flags & ~0x4) | (bool(value) << 2)
            self.pin_factory.connection.bb_spi_open(
                self._select_pin, self._miso_pin, self._mosi_pin, self._clock_pin,
                self._baud, self._spi_flags)

        def _get_lsb_first(self):
            return bool(self._spi_flags & 0xc000)

        def _set_lsb_first(self, value):
            self._check_open()
            self.pin_factory.connection.bb_spi_close(self._select_pin)
            self._spi_flags = (
                (self._spi_flags & ~0xc000)
                | (bool(value) << 14)
                | (bool(value) << 15)
                )
            self.pin_factory.connection.bb_spi_open(
                self._select_pin, self._miso_pin, self._mosi_pin, self._clock_pin,
                self._baud, self._spi_flags)

        def _get_rate(self):
            return self._baud

        def _set_rate(self, value):
            self._check_open()
            value = int(value)
            self.pin_factory.connection.bb_spi_close(self._select_pin)
            self._baud = value
            self.pin_factory.connection.bb_spi_open(
                self._select_pin, self._miso_pin, self._mosi_pin, self._clock_pin,
                self._baud, self._spi_flags)

        def transfer(self, data):
            self._check_open()
            count, data = self.pin_factory.connection.bb_spi_xfer(
                self._select_pin, data)
            if count < 0:
                raise IOError('SPI transfer error %d' % count)
            # Convert returned bytearray to list of ints. bb_spi only supports
            # byte-sized words so no issues here
            return [int(b) for b in data]

    class PiGPIOHardwareSPIShared(SharedMixin, PiGPIOHardwareSPI):
        @classmethod
        def _shared_key(cls, clock_pin, mosi_pin, miso_pin, select_pin, pin_factory):
            return (pin_factory.host, clock_pin, select_pin)

    class PiGPIOSoftwareSPIShared(SharedMixin, PiGPIOSoftwareSPI):
        @classmethod
        def _shared_key(cls, clock_pin, mosi_pin, miso_pin, select_pin, pin_factory):
            return (pin_factory.host, clock_pin, select_pin)