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 | @property | |
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 | def frequency( self ): | |
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 | if self._period == 0: | |
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 | ||
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() |