SHOW:
|
|
- or go back to the newest paste.
| 1 | from pathlib import Path | |
| 2 | ||
| 3 | def path_is_pwm_channel( path ): | |
| 4 | return path.resolve().match('pwmchip[0-9]*/pwm[0-9-]*')
| |
| 5 | ||
| 6 | class Pwm: | |
| 7 | def __init__( self, path, *, frequency=None, period=None, value=None, duty_cycle=None, enabled=None ): | |
| 8 | """path can either be absolute or relative to /dev/pwm/ | |
| 9 | ||
| 10 | Any remaining arguments are passed to configure() as-is. You should typically provide the desired | |
| 11 | frequency (or period) and initial value (or duty_cycle). | |
| 12 | """ | |
| 13 | path = Path( '/dev/pwm/', path ) | |
| 14 | self.path = path | |
| 15 | if not path.exists(): | |
| 16 | raise FileNotFoundError(f'Directory not found: {path}')
| |
| 17 | if not path.is_dir(): | |
| 18 | raise NotADirectoryError(f'Not a directory: {path}')
| |
| 19 | if not path_is_pwm_channel( path ): | |
| 20 | raise RuntimeError(f'Not a sysfs PWM channel: {path}')
| |
| 21 | ||
| 22 | self._enabled = bool( int( (path/'enable').read_text() ) ) | |
| 23 | self._period = int( (path/'period').read_text() ) | |
| 24 | self._duty_cycle = int( (path/'duty_cycle').read_text() ) | |
| 25 | ||
| 26 | self.configure( frequency=frequency, period=period, value=value, duty_cycle=duty_cycle, enabled=enabled ) | |
| 27 | ||
| 28 | def disable( self ): | |
| 29 | if not self._enabled: | |
| 30 | return | |
| 31 | (self.path/'enable').write_text('0')
| |
| 32 | self._enabled = False | |
| 33 | ||
| 34 | def enable( self ): | |
| 35 | if self._enabled: | |
| 36 | return | |
| 37 | if self._period == 0: | |
| 38 | raise RuntimeError("Cannot enable PWM when frequency is unconfigured (i.e. period is zero)")
| |
| 39 | (self.path/'enable').write_text('1')
| |
| 40 | self._enabled = True | |
| 41 | ||
| 42 | def configure( self, *, frequency=None, period=None, value=None, duty_cycle=None, enabled=None ): | |
| 43 | """Configure one or more PWM parameters. You can specify: | |
| 44 | ||
| 45 | - frequency (in Hz) or period (in ns) | |
| 46 | - value (in range 0.0-1.0) or duty_cycle (in ns) | |
| 47 | - enabled (bool) | |
| 48 | ||
| 49 | If frequency (or period) is specified then | |
| 50 | - value (or duty_cycle) must also be specified | |
| 51 | - enabled defaults to True | |
| 52 | Otherwise any parameters left unspecified are maintained unchanged. | |
| 53 | """ | |
| 54 | ||
| 55 | if frequency is not None or period is not None: | |
| 56 | if value is None and duty_cycle is None: | |
| 57 | raise RuntimeError("When configuring PWM frequency or period you must also specify value or duty_cycle")
| |
| 58 | if enabled is None: | |
| 59 | enabled = True | |
| 60 | else: | |
| 61 | if enabled is None: | |
| 62 | enabled = self._enabled | |
| 63 | ||
| 64 | if frequency is not None: | |
| 65 | if period is not None: | |
| 66 | raise RuntimeError("Cannot configure both PWM frequency and period")
| |
| 67 | if frequency <= 0: | |
| 68 | period = 2**32 | |
| 69 | else: | |
| 70 | period = round( 1e9 / frequency ) | |
| 71 | if period not in range( 1, 2**32 ): | |
| 72 | raise RuntimeError(f"PWM frequency must be in range {1e9/(2**32-1)} .. {1e9/1} Hz")
| |
| 73 | elif period is not None: | |
| 74 | period = round( period ) | |
| 75 | if period <= 0 or period >= 2**32: | |
| 76 | raise RuntimeError("PWM period must be in range 1 .. 4294967295 ns")
| |
| 77 | else: | |
| 78 | period = self._period | |
| 79 | ||
| 80 | if value is not None: | |
| 81 | if duty_cycle is not None: | |
| 82 | raise RuntimeError("Cannot configure both PWM value and duty_cycle")
| |
| 83 | if period == 0: | |
| 84 | raise RuntimeError("Cannot set PWM value when frequency is unconfigured (i.e. period is zero)")
| |
| 85 | if value < 0.0 or value > 1.0: | |
| 86 | raise RuntimeError("PWM value must be in range 0.0 .. 1.0")
| |
| 87 | duty_cycle = round( value * period ) | |
| 88 | elif duty_cycle is not None: | |
| 89 | duty_cycle = round( duty_cycle ) | |
| 90 | if duty_cycle < 0 or duty_cycle > period: | |
| 91 | raise RuntimeError(f"PWM duty_cycle must be in range 0 .. period ({period}) ns")
| |
| 92 | else: | |
| 93 | duty_cycle = self._duty_cycle | |
| 94 | ||
| 95 | if not enabled: | |
| 96 | self.disable() | |
| 97 | ||
| 98 | if duty_cycle < self._duty_cycle: | |
| 99 | (self.path/'duty_cycle').write_text( str( duty_cycle ) ) | |
| 100 | self._duty_cycle = int( (self.path/'duty_cycle').read_text() ) | |
| 101 | ||
| 102 | if period != self._period: | |
| 103 | (self.path/'period').write_text( str( period ) ) | |
| 104 | self._period = int( (self.path/'period').read_text() ) | |
| 105 | ||
| 106 | if duty_cycle != self._duty_cycle: | |
| 107 | (self.path/'duty_cycle').write_text( str( duty_cycle ) ) | |
| 108 | self._duty_cycle = int( (self.path/'duty_cycle').read_text() ) | |
| 109 | ||
| 110 | if enabled: | |
| 111 | self.enable() | |
| 112 | ||
| 113 | ||
| 114 | @property | |
| 115 | def enabled( self ): | |
| 116 | return self._enabled | |
| 117 | ||
| 118 | @enabled.setter | |
| 119 | def enabled( self, enabled ): | |
| 120 | if enabled: | |
| 121 | self.enable() | |
| 122 | else: | |
| 123 | self.disable() | |
| 124 | ||
| 125 | - | # period in nanoseconds |
| 125 | + | |
| 126 | def period( self ): | |
| 127 | return self._period | |
| 128 | ||
| 129 | @period.setter | |
| 130 | def period( self, period ): | |
| 131 | if self._duty_cycle > 0: | |
| 132 | raise RuntimeError("Cannot set period when PWM value is non-zero (i.e. duty_cycle is non-zero)")
| |
| 133 | self.configure( period=period, duty_cycle=0, enabled=self._enabled ) | |
| 134 | ||
| 135 | @property | |
| 136 | - | # frequency in Hz |
| 136 | + | |
| 137 | if self._period == 0: | |
| 138 | return None | |
| 139 | return 1e9 / self._period | |
| 140 | ||
| 141 | @frequency.setter | |
| 142 | def frequency( self, frequency ): | |
| 143 | if self._duty_cycle > 0: | |
| 144 | raise RuntimeError("Cannot set frequency when PWM value is non-zero (i.e. duty_cycle is non-zero)")
| |
| 145 | self.configure( frequency=frequency, duty_cycle=0, enabled=self._enabled ) | |
| 146 | ||
| 147 | @property | |
| 148 | def value( self ): | |
| 149 | - | # duty cycle as fraction of period (range 0.0 - 1.0) |
| 149 | + | |
| 150 | return None | |
| 151 | return self._duty_cycle / self._period | |
| 152 | ||
| 153 | @value.setter | |
| 154 | def value( self, value ): | |
| 155 | self.configure( value=value ) | |
| 156 | ||
| 157 | @property | |
| 158 | def duty_cycle( self ): | |
| 159 | return self._duty_cycle | |
| 160 | - | # duty cycle in nanoseconds |
| 160 | + | |
| 161 | @duty_cycle.setter | |
| 162 | def duty_cycle( self, duty_cycle ): | |
| 163 | self.configure( duty_cycle=duty_cycle ) | |
| 164 | ||
| 165 | ||
| 166 | # support being used as context manager to automatically disable pwm when exiting scope | |
| 167 | ||
| 168 | def __enter__( self ): | |
| 169 | return self | |
| 170 | ||
| 171 | def __exit__( self, exc_type, exc_val, exc_tb ): | |
| 172 | self.disable() |