1 # SPDX-FileCopyrightText: 2021 Melissa LeBlanc-Williams for Adafruit Industries
 
   3 # SPDX-License-Identifier: MIT
 
   5 Much code from https://github.com/vsergeev/python-periphery/blob/master/periphery/pwm.py
 
   6 Copyright (c) 2015-2016 vsergeev / Ivan (Vanya) A. Sergeev
 
  11 from time import sleep
 
  12 from errno import EACCES
 
  15     from microcontroller.pin import pwmOuts
 
  17     raise RuntimeError("No PWM outputs defined for this board.") from ImportError
 
  20 # pylint: disable=unnecessary-pass, too-many-instance-attributes
 
  23 class PWMError(IOError):
 
  24     """Base class for PWM errors."""
 
  29 # pylint: enable=unnecessary-pass
 
  33     """Pulse Width Modulation Output Class"""
 
  35     # Number of retries to check for successful PWM export on open
 
  37     # Delay between check for successful PWM export on open (100ms)
 
  41     _chip_path = "pwmchip{}"
 
  42     _channel_path = "pwm{}"
 
  44     def __init__(self, pwm, *, frequency=500, duty_cycle=0, variable_frequency=False):
 
  45         """Instantiate a PWM object and open the sysfs PWM corresponding to the
 
  46         specified chip and channel.
 
  49             frequency (int, float): target frequency in Hertz (32-bit).
 
  50             duty_cycle (int, float): The fraction of each pulse which is high (16-bit).
 
  51             variable_frequency (bool): True if the frequency will change over time.
 
  55             PWMError: if an I/O or OS error occurs.
 
  56             TypeError: if `chip` or `channel` types are invalid.
 
  57             LookupError: if PWM chip does not exist.
 
  58             TimeoutError: if waiting for PWM export times out.
 
  64         self._open(pwm, frequency, duty_cycle, variable_frequency)
 
  72     def __exit__(self, exc_type, exc_val, exc_tb):
 
  75     def _open(self, pwm, frequency, duty_cycle, variable_frequency):
 
  76         for pwmout in pwmOuts:
 
  78                 self._chip = pwmout[0][0]
 
  79                 self._channel = pwmout[0][1]
 
  81         self._chip_path = os.path.join(
 
  82             "/sys/class/pwm", self._chip_path.format(self._chip)
 
  84         self._channel_path = os.path.join(
 
  85             self._chip_path, self._channel_path.format(self._channel)
 
  88         if variable_frequency:
 
  89             print("Variable Frequency is not supported, continuing without it...")
 
  91         if not os.path.isdir(self._chip_path):
 
  92             raise LookupError("Opening PWM: PWM chip {} not found.".format(self._chip))
 
  94         if not os.path.isdir(self._channel_path):
 
  98                     os.path.join(self._chip_path, "export"), "w", encoding="utf-8"
 
 100                     f_export.write("{:d}\n".format(self._channel))
 
 103                     e.errno, "Exporting PWM channel: " + e.strerror
 
 106             # Loop until PWM is exported
 
 108             for i in range(PWMOut.PWM_STAT_RETRIES):
 
 109                 if os.path.isdir(self._channel_path):
 
 113                 sleep(PWMOut.PWM_STAT_DELAY)
 
 117                     'Exporting PWM: waiting for "{:s}" timed out.'.format(
 
 122             # Loop until 'period' is writable, This could take some time after
 
 123             # export as application of the udev rules after export is asynchronous.
 
 124             # Without this loop, the following properties may not be writable yet.
 
 125             for i in range(PWMOut.PWM_STAT_RETRIES):
 
 128                         os.path.join(self._channel_path, "period"),
 
 134                     if e.errno != EACCES or (
 
 135                         e.errno == EACCES and i == PWMOut.PWM_STAT_RETRIES - 1
 
 138                             e.errno, "Opening PWM period: " + e.strerror
 
 141                 sleep(PWMOut.PWM_STAT_DELAY)
 
 143             self.frequency = frequency
 
 144             self.duty_cycle = duty_cycle
 
 146             # Cache the period for fast duty cycle updates
 
 147             self._period_ns = self._get_period_ns()
 
 151         if self._channel is not None:
 
 152             # Unexporting the PWM channel
 
 154                 unexport_fd = os.open(
 
 155                     os.path.join(self._chip_path, "unexport"), os.O_WRONLY
 
 157                 os.write(unexport_fd, "{:d}\n".format(self._channel).encode())
 
 158                 os.close(unexport_fd)
 
 160                 raise PWMError(e.errno, "Unexporting PWM: " + e.strerror) from OSError
 
 165     def _write_channel_attr(self, attr, value):
 
 167             os.path.join(self._channel_path, attr), "w", encoding="utf-8"
 
 169             f_attr.write(value + "\n")
 
 171     def _read_channel_attr(self, attr):
 
 173             os.path.join(self._channel_path, attr), "r", encoding="utf-8"
 
 175             return f_attr.read().strip()
 
 180         """Enable the PWM output."""
 
 184         """Disable the PWM output."""
 
 189     def _get_period(self):
 
 190         return float(self.period_ms) / 1000
 
 192     def _set_period(self, period):
 
 193         if not isinstance(period, (int, float)):
 
 194             raise TypeError("Invalid period type, should be int.")
 
 196         self.period_ms = int(period * 1000)
 
 198     period = property(_get_period, _set_period)
 
 199     """Get or set the PWM's output period in seconds.
 
 202         PWMError: if an I/O or OS error occurs.
 
 203         TypeError: if value type is not int.
 
 208     def _get_period_ms(self):
 
 209         return self.period_us / 1000
 
 211     def _set_period_ms(self, period_ms):
 
 212         if not isinstance(period_ms, (int, float)):
 
 213             raise TypeError("Invalid period type, should be int or float.")
 
 214         self.period_us = int(period_ms * 1000)
 
 216     period_ms = property(_get_period_ms, _set_period_ms)
 
 217     """Get or set the PWM's output period in milliseconds.
 
 220         PWMError: if an I/O or OS error occurs.
 
 221         TypeError: if value type is not int.
 
 226     def _get_period_us(self):
 
 227         return self.period_ns / 1000
 
 229     def _set_period_us(self, period_us):
 
 230         if not isinstance(period_us, int):
 
 231             raise TypeError("Invalid period type, should be int.")
 
 233         self.period_ns = int(period_us * 1000)
 
 235     period_us = property(_get_period_us, _set_period_us)
 
 236     """Get or set the PWM's output period in microseconds.
 
 239         PWMError: if an I/O or OS error occurs.
 
 240         TypeError: if value type is not int.
 
 245     def _get_period_ns(self):
 
 246         period_ns = self._read_channel_attr("period")
 
 248             period_ns = int(period_ns)
 
 251                 None, 'Unknown period value: "%s".' % period_ns
 
 254         self._period_ns = period_ns
 
 258     def _set_period_ns(self, period_ns):
 
 259         if not isinstance(period_ns, int):
 
 260             raise TypeError("Invalid period type, should be int.")
 
 262         self._write_channel_attr("period", str(period_ns))
 
 264         # Update our cached period
 
 265         self._period_ns = period_ns
 
 267     period_ns = property(_get_period_ns, _set_period_ns)
 
 268     """Get or set the PWM's output period in nanoseconds.
 
 271         PWMError: if an I/O or OS error occurs.
 
 272         TypeError: if value type is not int.
 
 277     def _get_duty_cycle_ns(self):
 
 278         duty_cycle_ns_str = self._read_channel_attr("duty_cycle")
 
 281             duty_cycle_ns = int(duty_cycle_ns_str)
 
 284                 None, 'Unknown duty cycle value: "{:s}"'.format(duty_cycle_ns_str)
 
 289     def _set_duty_cycle_ns(self, duty_cycle_ns):
 
 290         if not isinstance(duty_cycle_ns, int):
 
 291             raise TypeError("Invalid duty cycle type, should be int.")
 
 293         self._write_channel_attr("duty_cycle", str(duty_cycle_ns))
 
 295     duty_cycle_ns = property(_get_duty_cycle_ns, _set_duty_cycle_ns)
 
 296     """Get or set the PWM's output duty cycle in nanoseconds.
 
 299         PWMError: if an I/O or OS error occurs.
 
 300         TypeError: if value type is not int.
 
 305     def _get_duty_cycle(self):
 
 306         return float(self.duty_cycle_ns) / self._period_ns
 
 308     def _set_duty_cycle(self, duty_cycle):
 
 309         if not isinstance(duty_cycle, (int, float)):
 
 310             raise TypeError("Invalid duty cycle type, should be int or float.")
 
 312         if not 0.0 <= duty_cycle <= 1.0:
 
 313             raise ValueError("Invalid duty cycle value, should be between 0.0 and 1.0.")
 
 315         # Convert duty cycle from ratio to nanoseconds
 
 316         self.duty_cycle_ns = int(duty_cycle * self._period_ns)
 
 318     duty_cycle = property(_get_duty_cycle, _set_duty_cycle)
 
 319     """Get or set the PWM's output duty cycle as a ratio from 0.0 to 1.0.
 
 321         PWMError: if an I/O or OS error occurs.
 
 322         TypeError: if value type is not int or float.
 
 323         ValueError: if value is out of bounds of 0.0 to 1.0.
 
 327     def _get_frequency(self):
 
 328         return 1.0 / self.period
 
 330     def _set_frequency(self, frequency):
 
 331         if not isinstance(frequency, (int, float)):
 
 332             raise TypeError("Invalid frequency type, should be int or float.")
 
 334         self.period = 1.0 / frequency
 
 336     frequency = property(_get_frequency, _set_frequency)
 
 337     """Get or set the PWM's output frequency in Hertz.
 
 339         PWMError: if an I/O or OS error occurs.
 
 340         TypeError: if value type is not int or float.
 
 344     def _get_polarity(self):
 
 345         return self._read_channel_attr("polarity")
 
 347     def _set_polarity(self, polarity):
 
 348         if not isinstance(polarity, str):
 
 349             raise TypeError("Invalid polarity type, should be str.")
 
 351         if polarity.lower() not in ["normal", "inversed"]:
 
 352             raise ValueError('Invalid polarity, can be: "normal" or "inversed".')
 
 354         self._write_channel_attr("polarity", polarity.lower())
 
 356     polarity = property(_get_polarity, _set_polarity)
 
 357     """Get or set the PWM's output polarity. Can be "normal" or "inversed".
 
 359         PWMError: if an I/O or OS error occurs.
 
 360         TypeError: if value type is not str.
 
 361         ValueError: if value is invalid.
 
 365     def _get_enabled(self):
 
 366         enabled = self._read_channel_attr("enable")
 
 373         raise PWMError(None, 'Unknown enabled value: "{:s}"'.format(enabled))
 
 375     def _set_enabled(self, value):
 
 376         if not isinstance(value, bool):
 
 377             raise TypeError("Invalid enabled type, should be bool.")
 
 379         self._write_channel_attr("enable", "1" if value else "0")
 
 381     enabled = property(_get_enabled, _set_enabled)
 
 382     """Get or set the PWM's output enabled state.
 
 384         PWMError: if an I/O or OS error occurs.
 
 385         TypeError: if value type is not bool.
 
 389     # String representation
 
 393             "PWM {:d}, chip {:d} (period={:f} sec, duty_cycle={:f}%,"
 
 394             " polarity={:s}, enabled={:s})".format(
 
 398                 self.duty_cycle * 100,