Coverage for /usr/lib/python3/dist-packages/gpiozero/mixins.py: 43%

250 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-04-05 16:40 +0100

1# vim: set fileencoding=utf-8: 

2# 

3# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins 

4# 

5# Copyright (c) 2016-2021 Dave Jones <dave@waveform.org.uk> 

6# Copyright (c) 2018-2021 Ben Nuttall <ben@bennuttall.com> 

7# Copyright (c) 2016 Andrew Scheller <github@loowis.durge.org> 

8# 

9# SPDX-License-Identifier: BSD-3-Clause 

10 

11from __future__ import ( 

12 unicode_literals, 

13 print_function, 

14 absolute_import, 

15 division, 

16 ) 

17nstr = str 

18str = type('') 

19 

20import inspect 

21import weakref 

22from functools import wraps, partial 

23from threading import Event 

24from collections import deque 

25try: 

26 from statistics import median 

27except ImportError: 

28 from .compat import median 

29import warnings 

30 

31from .threads import GPIOThread 

32from .exc import ( 

33 BadEventHandler, 

34 BadWaitTime, 

35 BadQueueLen, 

36 DeviceClosed, 

37 CallbackSetToNone, 

38 ) 

39 

40callback_warning = ( 

41 'The callback was set to None. This may have been unintentional ' 

42 'e.g. btn.when_pressed = pressed() instead of btn.when_pressed = pressed' 

43) 

44 

45class ValuesMixin(object): 

46 """ 

47 Adds a :attr:`values` property to the class which returns an infinite 

48 generator of readings from the :attr:`~Device.value` property. There is 

49 rarely a need to use this mixin directly as all base classes in GPIO Zero 

50 include it. 

51 

52 .. note:: 

53 

54 Use this mixin *first* in the parent class list. 

55 """ 

56 

57 @property 

58 def values(self): 

59 """ 

60 An infinite iterator of values read from :attr:`value`. 

61 """ 

62 while True: 

63 try: 

64 yield self.value 

65 except DeviceClosed: 

66 break 

67 

68 

69class SourceMixin(object): 

70 """ 

71 Adds a :attr:`source` property to the class which, given an iterable or a 

72 :class:`ValuesMixin` descendent, sets :attr:`~Device.value` to each member 

73 of that iterable until it is exhausted. This mixin is generally included in 

74 novel output devices to allow their state to be driven from another device. 

75 

76 .. note:: 

77 

78 Use this mixin *first* in the parent class list. 

79 """ 

80 

81 def __init__(self, *args, **kwargs): 

82 self._source = None 

83 self._source_thread = None 

84 self._source_delay = 0.01 

85 super(SourceMixin, self).__init__(*args, **kwargs) 

86 

87 def close(self): 

88 self.source = None 

89 super(SourceMixin, self).close() 

90 

91 def _copy_values(self, source): 

92 for v in source: 

93 self.value = v 

94 if self._source_thread.stopping.wait(self._source_delay): 

95 break 

96 

97 @property 

98 def source_delay(self): 

99 """ 

100 The delay (measured in seconds) in the loop used to read values from 

101 :attr:`source`. Defaults to 0.01 seconds which is generally sufficient 

102 to keep CPU usage to a minimum while providing adequate responsiveness. 

103 """ 

104 return self._source_delay 

105 

106 @source_delay.setter 

107 def source_delay(self, value): 

108 if value < 0: 

109 raise BadWaitTime('source_delay must be 0 or greater') 

110 self._source_delay = float(value) 

111 

112 @property 

113 def source(self): 

114 """ 

115 The iterable to use as a source of values for :attr:`value`. 

116 """ 

117 return self._source 

118 

119 @source.setter 

120 def source(self, value): 

121 if getattr(self, '_source_thread', None): 

122 self._source_thread.stop() 

123 self._source_thread = None 

124 if isinstance(value, ValuesMixin): 

125 value = value.values 

126 self._source = value 

127 if value is not None: 

128 self._source_thread = GPIOThread(self._copy_values, (value,)) 

129 self._source_thread.start() 

130 

131 

132class SharedMixin(object): 

133 """ 

134 This mixin marks a class as "shared". In this case, the meta-class 

135 (GPIOMeta) will use :meth:`_shared_key` to convert the constructor 

136 arguments to an immutable key, and will check whether any existing 

137 instances match that key. If they do, they will be returned by the 

138 constructor instead of a new instance. An internal reference counter is 

139 used to determine how many times an instance has been "constructed" in this 

140 way. 

141 

142 When :meth:`~Device.close` is called, an internal reference counter will be 

143 decremented and the instance will only close when it reaches zero. 

144 """ 

145 _instances = {} 

146 

147 def __del__(self): 

148 self._refs = 0 

149 super(SharedMixin, self).__del__() 

150 

151 @classmethod 

152 def _shared_key(cls, *args, **kwargs): 

153 """ 

154 This is called with the constructor arguments to generate a unique 

155 key (which must be storable in a :class:`dict` and, thus, immutable 

156 and hashable) representing the instance that can be shared. This must 

157 be overridden by descendents. 

158 

159 The default simply assumes all positional arguments are immutable and 

160 returns this as the key but this is almost never the "right" thing to 

161 do and almost all descendents should override this method. 

162 """ 

163 # XXX Future 2.x version should change this to raise NotImplementedError 

164 return args 

165 

166 

167class event(object): 

168 """ 

169 A descriptor representing a callable event on a class descending from 

170 :class:`EventsMixin`. 

171 

172 Instances of this class are very similar to a :class:`property` but also 

173 deal with notifying the owning class when events are assigned (or 

174 unassigned) and wrapping callbacks implicitly as appropriate. 

175 """ 

176 def __init__(self, doc=None): 

177 self.handlers = {} 

178 self.__doc__ = doc 

179 

180 def __get__(self, instance, owner=None): 

181 if instance is None: 181 ↛ 184line 181 didn't jump to line 184, because the condition on line 181 was never false

182 return self 

183 else: 

184 return self.handlers.get(id(instance)) 

185 

186 def __set__(self, instance, value): 

187 if value is None: 187 ↛ 188line 187 didn't jump to line 188, because the condition on line 187 was never true

188 try: 

189 del self.handlers[id(instance)] 

190 except KeyError: 

191 warnings.warn(CallbackSetToNone(callback_warning)) 

192 else: 

193 self.handlers[id(instance)] = instance._wrap_callback(value) 

194 enabled = any( 194 ↛ exitline 194 didn't jump to the function exit

195 obj.handlers.get(id(instance)) 

196 for name in dir(type(instance)) 

197 for obj in (getattr(type(instance), name),) 

198 if isinstance(obj, event) 

199 ) 

200 instance._start_stop_events(enabled) 

201 

202 

203class EventsMixin(object): 

204 """ 

205 Adds edge-detected :meth:`when_activated` and :meth:`when_deactivated` 

206 events to a device based on changes to the :attr:`~Device.is_active` 

207 property common to all devices. Also adds :meth:`wait_for_active` and 

208 :meth:`wait_for_inactive` methods for level-waiting. 

209 

210 .. note:: 

211 

212 Note that this mixin provides no means of actually firing its events; 

213 call :meth:`_fire_events` in sub-classes when device state changes to 

214 trigger the events. This should also be called once at the end of 

215 initialization to set initial states. 

216 """ 

217 def __init__(self, *args, **kwargs): 

218 super(EventsMixin, self).__init__(*args, **kwargs) 

219 self._active_event = Event() 

220 self._inactive_event = Event() 

221 self._last_active = None 

222 self._last_changed = self.pin_factory.ticks() 

223 

224 def _all_events(self): 

225 """ 

226 Generator function which yields all :class:`event` instances defined 

227 against this class. 

228 """ 

229 for name in dir(type(self)): 

230 obj = getattr(type(self), name) 

231 if isinstance(obj, event): 

232 yield obj 

233 

234 def close(self): 

235 for ev in self._all_events(): 

236 try: 

237 del ev.handlers[id(self)] 

238 except KeyError: 

239 pass 

240 super(EventsMixin, self).close() 

241 

242 def wait_for_active(self, timeout=None): 

243 """ 

244 Pause the script until the device is activated, or the timeout is 

245 reached. 

246 

247 :type timeout: float or None 

248 :param timeout: 

249 Number of seconds to wait before proceeding. If this is 

250 :data:`None` (the default), then wait indefinitely until the device 

251 is active. 

252 """ 

253 return self._active_event.wait(timeout) 

254 

255 def wait_for_inactive(self, timeout=None): 

256 """ 

257 Pause the script until the device is deactivated, or the timeout is 

258 reached. 

259 

260 :type timeout: float or None 

261 :param timeout: 

262 Number of seconds to wait before proceeding. If this is 

263 :data:`None` (the default), then wait indefinitely until the device 

264 is inactive. 

265 """ 

266 return self._inactive_event.wait(timeout) 

267 

268 when_activated = event( 

269 """ 

270 The function to run when the device changes state from inactive to 

271 active. 

272 

273 This can be set to a function which accepts no (mandatory) parameters, 

274 or a Python function which accepts a single mandatory parameter (with 

275 as many optional parameters as you like). If the function accepts a 

276 single mandatory parameter, the device that activated it will be passed 

277 as that parameter. 

278 

279 Set this property to :data:`None` (the default) to disable the event. 

280 """) 

281 

282 when_deactivated = event( 

283 """ 

284 The function to run when the device changes state from active to 

285 inactive. 

286 

287 This can be set to a function which accepts no (mandatory) parameters, 

288 or a Python function which accepts a single mandatory parameter (with 

289 as many optional parameters as you like). If the function accepts a 

290 single mandatory parameter, the device that deactivated it will be 

291 passed as that parameter. 

292 

293 Set this property to :data:`None` (the default) to disable the event. 

294 """) 

295 

296 @property 

297 def active_time(self): 

298 """ 

299 The length of time (in seconds) that the device has been active for. 

300 When the device is inactive, this is :data:`None`. 

301 """ 

302 if self._active_event.is_set(): 

303 return self.pin_factory.ticks_diff(self.pin_factory.ticks(), 

304 self._last_changed) 

305 else: 

306 return None 

307 

308 @property 

309 def inactive_time(self): 

310 """ 

311 The length of time (in seconds) that the device has been inactive for. 

312 When the device is active, this is :data:`None`. 

313 """ 

314 if self._inactive_event.is_set(): 

315 return self.pin_factory.ticks_diff(self.pin_factory.ticks(), 

316 self._last_changed) 

317 else: 

318 return None 

319 

320 def _wrap_callback(self, fn): 

321 # XXX In 2.x, move this to the event class above 

322 if not callable(fn): 322 ↛ 323line 322 didn't jump to line 323, because the condition on line 322 was never true

323 raise BadEventHandler('value must be None or a callable') 

324 # If fn is wrapped with partial (i.e. partial, partialmethod, or wraps 

325 # has been used to produce it) we need to dig out the "real" function 

326 # that's been wrapped along with all the mandatory positional args 

327 # used in the wrapper so we can test the binding 

328 args = () 

329 wrapped_fn = fn 

330 while isinstance(wrapped_fn, partial): 330 ↛ 331line 330 didn't jump to line 331, because the condition on line 330 was never true

331 args = wrapped_fn.args + args 

332 wrapped_fn = wrapped_fn.func 

333 if inspect.isbuiltin(wrapped_fn): 333 ↛ 339line 333 didn't jump to line 339, because the condition on line 333 was never true

334 # We can't introspect the prototype of builtins. In this case we 

335 # assume that the builtin has no (mandatory) parameters; this is 

336 # the most reasonable assumption on the basis that pre-existing 

337 # builtins have no knowledge of gpiozero, and the sole parameter 

338 # we would pass is a gpiozero object 

339 return fn 

340 else: 

341 # Try binding ourselves to the argspec of the provided callable. 

342 # If this works, assume the function is capable of accepting no 

343 # parameters 

344 try: 

345 inspect.getcallargs(wrapped_fn, *args) 

346 return fn 

347 except TypeError: 

348 try: 

349 # If the above fails, try binding with a single parameter 

350 # (ourselves). If this works, wrap the specified callback 

351 inspect.getcallargs(wrapped_fn, *(args + (self,))) 

352 @wraps(fn) 

353 def wrapper(): 

354 return fn(self) 

355 return wrapper 

356 except TypeError: 

357 raise BadEventHandler( 

358 'value must be a callable which accepts up to one ' 

359 'mandatory parameter') 

360 

361 def _fire_activated(self): 

362 # These methods are largely here to be overridden by descendents 

363 if self.when_activated: 

364 self.when_activated() 

365 

366 def _fire_deactivated(self): 

367 # These methods are largely here to be overridden by descendents 

368 if self.when_deactivated: 

369 self.when_deactivated() 

370 

371 def _fire_events(self, ticks, new_active): 

372 """ 

373 This method should be called by descendents whenever the 

374 :attr:`~Device.is_active` property is likely to have changed (for 

375 example, in response to a pin's :attr:`~gpiozero.Pin.state` changing). 

376 

377 The *ticks* parameter must be set to the time when the change occurred; 

378 this can usually be obtained from the pin factory's 

379 :meth:`gpiozero.Factory.ticks` method but some pin implementations will 

380 implicitly provide the ticks when an event occurs as part of their 

381 reporting mechanism. 

382 

383 The *new_active* parameter must be set to the device's 

384 :attr:`~Device.is_active` value at the time indicated by *ticks* (which 

385 is not necessarily the value of :attr:`~Device.is_active` right now, if 

386 the pin factory provides means of reporting a pin's historical state). 

387 """ 

388 old_active, self._last_active = self._last_active, new_active 

389 if old_active is None: 389 ↛ 396line 389 didn't jump to line 396, because the condition on line 389 was never false

390 # Initial "indeterminate" state; set events but don't fire 

391 # callbacks as there's not necessarily an edge 

392 if new_active: 392 ↛ 393line 392 didn't jump to line 393, because the condition on line 392 was never true

393 self._active_event.set() 

394 else: 

395 self._inactive_event.set() 

396 elif old_active != new_active: 

397 self._last_changed = ticks 

398 if new_active: 

399 self._inactive_event.clear() 

400 self._active_event.set() 

401 self._fire_activated() 

402 else: 

403 self._active_event.clear() 

404 self._inactive_event.set() 

405 self._fire_deactivated() 

406 

407 def _start_stop_events(self, enabled): 

408 """ 

409 This is a stub method that only exists to be overridden by descendents. 

410 It is called when :class:`event` properties are assigned (including 

411 when set to :data:`None) to permit the owning instance to activate or 

412 deactivate monitoring facilities. 

413 

414 For example, if a descendent requires a background thread to monitor a 

415 device, it would be preferable to only run the thread if event handlers 

416 are present to respond to it. 

417 

418 The *enabled* parameter is :data:`False` when all :class:`event` 

419 properties on the owning class are :data:`None`, and :data:`True` 

420 otherwise. 

421 """ 

422 pass 

423 

424 

425class HoldMixin(EventsMixin): 

426 """ 

427 Extends :class:`EventsMixin` to add the :attr:`when_held` event and the 

428 machinery to fire that event repeatedly (when :attr:`hold_repeat` is 

429 :data:`True`) at internals defined by :attr:`hold_time`. 

430 """ 

431 def __init__(self, *args, **kwargs): 

432 self._hold_thread = None 

433 super(HoldMixin, self).__init__(*args, **kwargs) 

434 self._when_held = None 

435 self._held_from = None 

436 self._hold_time = 1 

437 self._hold_repeat = False 

438 self._hold_thread = HoldThread(self) 

439 

440 def close(self): 

441 if self._hold_thread is not None: 

442 self._hold_thread.stop() 

443 self._hold_thread = None 

444 super(HoldMixin, self).close() 

445 

446 def _fire_activated(self): 

447 super(HoldMixin, self)._fire_activated() 

448 self._hold_thread.holding.set() 

449 

450 def _fire_deactivated(self): 

451 self._held_from = None 

452 super(HoldMixin, self)._fire_deactivated() 

453 

454 def _fire_held(self): 

455 if self.when_held: 

456 self.when_held() 

457 

458 when_held = event( 

459 """ 

460 The function to run when the device has remained active for 

461 :attr:`hold_time` seconds. 

462 

463 This can be set to a function which accepts no (mandatory) parameters, 

464 or a Python function which accepts a single mandatory parameter (with 

465 as many optional parameters as you like). If the function accepts a 

466 single mandatory parameter, the device that activated will be passed 

467 as that parameter. 

468 

469 Set this property to :data:`None` (the default) to disable the event. 

470 """) 

471 

472 @property 

473 def hold_time(self): 

474 """ 

475 The length of time (in seconds) to wait after the device is activated, 

476 until executing the :attr:`when_held` handler. If :attr:`hold_repeat` 

477 is True, this is also the length of time between invocations of 

478 :attr:`when_held`. 

479 """ 

480 return self._hold_time 

481 

482 @hold_time.setter 

483 def hold_time(self, value): 

484 if value < 0: 484 ↛ 485line 484 didn't jump to line 485, because the condition on line 484 was never true

485 raise BadWaitTime('hold_time must be 0 or greater') 

486 self._hold_time = float(value) 

487 

488 @property 

489 def hold_repeat(self): 

490 """ 

491 If :data:`True`, :attr:`when_held` will be executed repeatedly with 

492 :attr:`hold_time` seconds between each invocation. 

493 """ 

494 return self._hold_repeat 

495 

496 @hold_repeat.setter 

497 def hold_repeat(self, value): 

498 self._hold_repeat = bool(value) 

499 

500 @property 

501 def is_held(self): 

502 """ 

503 When :data:`True`, the device has been active for at least 

504 :attr:`hold_time` seconds. 

505 """ 

506 return self._held_from is not None 

507 

508 @property 

509 def held_time(self): 

510 """ 

511 The length of time (in seconds) that the device has been held for. 

512 This is counted from the first execution of the :attr:`when_held` event 

513 rather than when the device activated, in contrast to 

514 :attr:`~EventsMixin.active_time`. If the device is not currently held, 

515 this is :data:`None`. 

516 """ 

517 if self._held_from is not None: 

518 return self.pin_factory.ticks_diff(self.pin_factory.ticks(), 

519 self._held_from) 

520 else: 

521 return None 

522 

523 

524class HoldThread(GPIOThread): 

525 """ 

526 Extends :class:`GPIOThread`. Provides a background thread that repeatedly 

527 fires the :attr:`HoldMixin.when_held` event as long as the owning 

528 device is active. 

529 """ 

530 def __init__(self, parent): 

531 super(HoldThread, self).__init__( 

532 target=self.held, args=(weakref.proxy(parent),)) 

533 self.holding = Event() 

534 self.start() 

535 

536 def held(self, parent): 

537 try: 

538 while not self.stopping.is_set(): 

539 if self.holding.wait(0.1): 539 ↛ 540line 539 didn't jump to line 540, because the condition on line 539 was never true

540 self.holding.clear() 

541 while not ( 

542 self.stopping.is_set() or 

543 parent._inactive_event.wait(parent.hold_time) 

544 ): 

545 if parent._held_from is None: 

546 parent._held_from = parent.pin_factory.ticks() 

547 parent._fire_held() 

548 if not parent.hold_repeat: 

549 break 

550 except ReferenceError: 

551 # Parent is dead; time to die! 

552 pass 

553 

554 

555class GPIOQueue(GPIOThread): 

556 """ 

557 Extends :class:`GPIOThread`. Provides a background thread that monitors a 

558 device's values and provides a running *average* (defaults to median) of 

559 those values. If the *parent* device includes the :class:`EventsMixin` in 

560 its ancestry, the thread automatically calls 

561 :meth:`~EventsMixin._fire_events`. 

562 """ 

563 def __init__( 

564 self, parent, queue_len=5, sample_wait=0.0, partial=False, 

565 average=median, ignore=None): 

566 assert callable(average) 

567 if queue_len < 1: 

568 raise BadQueueLen('queue_len must be at least one') 

569 if sample_wait < 0: 

570 raise BadWaitTime('sample_wait must be 0 or greater') 

571 if ignore is None: 

572 ignore = set() 

573 super(GPIOQueue, self).__init__(target=self.fill) 

574 self.queue = deque(maxlen=queue_len) 

575 self.partial = bool(partial) 

576 self.sample_wait = float(sample_wait) 

577 self.full = Event() 

578 self.parent = weakref.proxy(parent) 

579 self.average = average 

580 self.ignore = ignore 

581 

582 @property 

583 def value(self): 

584 if not self.partial: 

585 self.full.wait() 

586 try: 

587 return self.average(self.queue) 

588 except (ZeroDivisionError, ValueError): 

589 # No data == inactive value 

590 return 0.0 

591 

592 def fill(self): 

593 try: 

594 while not self.stopping.wait(self.sample_wait): 

595 value = self.parent._read() 

596 if value not in self.ignore: 

597 self.queue.append(value) 

598 if not self.full.is_set() and len(self.queue) >= self.queue.maxlen: 

599 self.full.set() 

600 if (self.partial or self.full.is_set()) and isinstance(self.parent, EventsMixin): 

601 self.parent._fire_events(self.parent.pin_factory.ticks(), self.parent.is_active) 

602 except ReferenceError: 

603 # Parent is dead; time to die! 

604 pass