View difference between Paste ID: DUfcTcMR and R70P1wAn
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()