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
 
 145             self.polarity = "normal"
 
 148             # Cache the period for fast duty cycle updates
 
 149             self._period_ns = self._get_period_ns()
 
 153         if self._channel is not None:
 
 154             # Unexporting the PWM channel
 
 156                 unexport_fd = os.open(
 
 157                     os.path.join(self._chip_path, "unexport"), os.O_WRONLY
 
 159                 os.write(unexport_fd, "{:d}\n".format(self._channel).encode())
 
 160                 os.close(unexport_fd)
 
 162                 raise PWMError(e.errno, "Unexporting PWM: " + e.strerror) from OSError
 
 167     def _write_channel_attr(self, attr, value):
 
 169             os.path.join(self._channel_path, attr), "w", encoding="utf-8"
 
 171             f_attr.write(value + "\n")
 
 173     def _read_channel_attr(self, attr):
 
 175             os.path.join(self._channel_path, attr), "r", encoding="utf-8"
 
 177             return f_attr.read().strip()
 
 182         """Enable the PWM output."""
 
 186         """Disable the PWM output."""
 
 191     def _get_period(self):
 
 192         return float(self.period_ms) / 1000
 
 194     def _set_period(self, period):
 
 195         if not isinstance(period, (int, float)):
 
 196             raise TypeError("Invalid period type, should be int.")
 
 198         self.period_ms = int(period * 1000)
 
 200     period = property(_get_period, _set_period)
 
 201     """Get or set the PWM's output period in seconds.
 
 204         PWMError: if an I/O or OS error occurs.
 
 205         TypeError: if value type is not int.
 
 210     def _get_period_ms(self):
 
 211         return self.period_us / 1000
 
 213     def _set_period_ms(self, period_ms):
 
 214         if not isinstance(period_ms, (int, float)):
 
 215             raise TypeError("Invalid period type, should be int or float.")
 
 216         self.period_us = int(period_ms * 1000)
 
 218     period_ms = property(_get_period_ms, _set_period_ms)
 
 219     """Get or set the PWM's output period in milliseconds.
 
 222         PWMError: if an I/O or OS error occurs.
 
 223         TypeError: if value type is not int.
 
 228     def _get_period_us(self):
 
 229         return self.period_ns / 1000
 
 231     def _set_period_us(self, period_us):
 
 232         if not isinstance(period_us, int):
 
 233             raise TypeError("Invalid period type, should be int.")
 
 235         self.period_ns = int(period_us * 1000)
 
 237     period_us = property(_get_period_us, _set_period_us)
 
 238     """Get or set the PWM's output period in microseconds.
 
 241         PWMError: if an I/O or OS error occurs.
 
 242         TypeError: if value type is not int.
 
 247     def _get_period_ns(self):
 
 248         period_ns = self._read_channel_attr("period")
 
 250             period_ns = int(period_ns)
 
 253                 None, 'Unknown period value: "%s".' % period_ns
 
 256         self._period_ns = period_ns
 
 260     def _set_period_ns(self, period_ns):
 
 261         if not isinstance(period_ns, int):
 
 262             raise TypeError("Invalid period type, should be int.")
 
 264         self._write_channel_attr("period", str(period_ns))
 
 266         # Update our cached period
 
 267         self._period_ns = period_ns
 
 269     period_ns = property(_get_period_ns, _set_period_ns)
 
 270     """Get or set the PWM's output period in nanoseconds.
 
 273         PWMError: if an I/O or OS error occurs.
 
 274         TypeError: if value type is not int.
 
 279     def _get_duty_cycle_ns(self):
 
 280         duty_cycle_ns_str = self._read_channel_attr("duty_cycle")
 
 283             duty_cycle_ns = int(duty_cycle_ns_str)
 
 286                 None, 'Unknown duty cycle value: "{:s}"'.format(duty_cycle_ns_str)
 
 291     def _set_duty_cycle_ns(self, duty_cycle_ns):
 
 292         if not isinstance(duty_cycle_ns, int):
 
 293             raise TypeError("Invalid duty cycle type, should be int.")
 
 295         self._write_channel_attr("duty_cycle", str(duty_cycle_ns))
 
 297     duty_cycle_ns = property(_get_duty_cycle_ns, _set_duty_cycle_ns)
 
 298     """Get or set the PWM's output duty cycle in nanoseconds.
 
 301         PWMError: if an I/O or OS error occurs.
 
 302         TypeError: if value type is not int.
 
 307     def _get_duty_cycle(self):
 
 308         return float(self.duty_cycle_ns) / self._period_ns
 
 310     def _set_duty_cycle(self, duty_cycle):
 
 311         if not isinstance(duty_cycle, (int, float)):
 
 312             raise TypeError("Invalid duty cycle type, should be int or float.")
 
 314         if not 0.0 <= duty_cycle <= 1.0:
 
 315             raise ValueError("Invalid duty cycle value, should be between 0.0 and 1.0.")
 
 317         # Convert duty cycle from ratio to nanoseconds
 
 318         self.duty_cycle_ns = int(duty_cycle * self._period_ns)
 
 320     duty_cycle = property(_get_duty_cycle, _set_duty_cycle)
 
 321     """Get or set the PWM's output duty cycle as a ratio from 0.0 to 1.0.
 
 323         PWMError: if an I/O or OS error occurs.
 
 324         TypeError: if value type is not int or float.
 
 325         ValueError: if value is out of bounds of 0.0 to 1.0.
 
 329     def _get_frequency(self):
 
 330         return 1.0 / self.period
 
 332     def _set_frequency(self, frequency):
 
 333         if not isinstance(frequency, (int, float)):
 
 334             raise TypeError("Invalid frequency type, should be int or float.")
 
 336         self.period = 1.0 / frequency
 
 338     frequency = property(_get_frequency, _set_frequency)
 
 339     """Get or set the PWM's output frequency in Hertz.
 
 341         PWMError: if an I/O or OS error occurs.
 
 342         TypeError: if value type is not int or float.
 
 346     def _get_polarity(self):
 
 347         return self._read_channel_attr("polarity")
 
 349     def _set_polarity(self, polarity):
 
 350         if not isinstance(polarity, str):
 
 351             raise TypeError("Invalid polarity type, should be str.")
 
 353         if polarity.lower() not in ["normal", "inversed"]:
 
 354             raise ValueError('Invalid polarity, can be: "normal" or "inversed".')
 
 356         self._write_channel_attr("polarity", polarity.lower())
 
 358     polarity = property(_get_polarity, _set_polarity)
 
 359     """Get or set the PWM's output polarity. Can be "normal" or "inversed".
 
 361         PWMError: if an I/O or OS error occurs.
 
 362         TypeError: if value type is not str.
 
 363         ValueError: if value is invalid.
 
 367     def _get_enabled(self):
 
 368         enabled = self._read_channel_attr("enable")
 
 375         raise PWMError(None, 'Unknown enabled value: "{:s}"'.format(enabled))
 
 377     def _set_enabled(self, value):
 
 378         if not isinstance(value, bool):
 
 379             raise TypeError("Invalid enabled type, should be bool.")
 
 381         self._write_channel_attr("enable", "1" if value else "0")
 
 383     enabled = property(_get_enabled, _set_enabled)
 
 384     """Get or set the PWM's output enabled state.
 
 386         PWMError: if an I/O or OS error occurs.
 
 387         TypeError: if value type is not bool.
 
 391     # String representation
 
 395             "PWM {:d}, chip {:d} (period={:f} sec, duty_cycle={:f}%,"
 
 396             " polarity={:s}, enabled={:s})".format(
 
 400                 self.duty_cycle * 100,