Skip to content

Commit db21199

Browse files
committed
Encoder primitive now has div arg.
1 parent 3661ee9 commit db21199

File tree

2 files changed

+75
-33
lines changed

2 files changed

+75
-33
lines changed

v3/docs/DRIVERS.md

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -353,15 +353,22 @@ must be tracked.
353353

354354
This driver runs the user supplied callback in an `asyncio` context, so it runs
355355
only when other tasks have yielded to the scheduler. This ensures that the
356-
callback can run safely. The driver allows limits to be assigned to the control
357-
so that a dial running from (say) 0 to 100 may be implemented. If limits are
358-
used, encoder values no longer represent absolute angles.
356+
callback can run safely, even if it triggers complex application behaviour.
359357

360-
The callback only runs if a change in position has occurred.
358+
The `Encoder` can be instantiated in such a way that its effective resolution
359+
can be reduced. A virtual encoder with lower resolution can be useful in some
360+
applications.
361361

362-
A consequence of the callback running in an `asyncio` context is that, by the
363-
time it runs, the encoder's position may have changed by more than one
364-
increment.
362+
The driver allows limits to be assigned to the virtual encoder's value so that
363+
a dial running from (say) 0 to 100 may be implemented. If limits arenused,
364+
encoder values no longer represent absolute angles, as the user might continue
365+
to rotate the dial when it is "stuck" at an endstop.
366+
367+
The callback only runs if a change in position of the virtual encoder has
368+
occurred. In consequence of the callback running in an `asyncio` context, by
369+
the time it is scheduled, the encoder's position may have changed by more than
370+
one increment. The callback receives two args, the absolute value of the
371+
virtual encoder and the signed change since the previous callback run.
365372

366373
## 6.1 Encoder class
367374

@@ -372,18 +379,39 @@ Constructor arguments:
372379
3. `v=0` Initial value.
373380
4. `vmin=None` By default the `value` of the encoder can vary without limit.
374381
Optionally maximum and/or minimum limits can be set.
375-
5. `vmax=None`
376-
6. `callback=lambda *_ : None` Optional callback function. The callback
382+
5. `vmax=None` As above. If `vmin` and/or `vmax` are specified, a `ValueError`
383+
will be thrown if the initial value `v` does not conform with the limits.
384+
6. `div=1` A value > 1 causes the motion rate of the encoder to be divided
385+
down, to produce a virtual encoder with lower resolution. This was found usefl
386+
in some applications with the Adafruit encoder.
387+
7. `callback=lambda a, b : None` Optional callback function. The callback
377388
receives two args, `v` being the encoder's current value and `delta` being
378389
the signed difference between the current value and the previous one. Further
379390
args may be appended by the following.
380-
7. `args=()` An optional tuple of args for the callback.
391+
8. `args=()` An optional tuple of args for the callback.
381392

382393
Synchronous method:
383394
* `value` No args. Returns an integer being the `Encoder` current value.
384395

385396
Class variable:
386-
* `LATENCY=50` This sets a minumum period (in ms) between callback runs.
397+
* `delay=100` After motion is detected the driver waits for `delay` ms before
398+
reading the current position. This was found useful with the Adafruit encoder
399+
which has mechanical detents, which span multiple increments or decrements. A
400+
delay gives time for motion to stop, enabling just one call to the callback.
401+
402+
#### Note
403+
404+
The driver works by maintaining an internal value `._v` which uses hardware
405+
interrupts to track the absolute position of the physical encoder. In theory
406+
this should be precise, but on ESP32 with the Adafruit encoder it is not.
407+
408+
Currently under investigation: it may be a consequence of ESP32's use of soft
409+
IRQ's.
410+
411+
This is probably of little practical consequence as encoder knobs are usually
412+
used in systems where there is user feedback. In a practical application
413+
([ugui](https://github.com/peterhinch/micropython-micro-gui)) I can see no
414+
evidence of the missed pulses.
387415

388416
###### [Contents](./DRIVERS.md#1-contents)
389417

v3/primitives/encoder.py

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,32 @@
33
# Copyright (c) 2021 Peter Hinch
44
# Released under the MIT License (MIT) - see LICENSE file
55

6-
# This driver is intended for encoder-based control knobs. It is not
7-
# suitable for NC machine applications. Please see the docs.
6+
# This driver is intended for encoder-based control knobs. It is
7+
# unsuitable for NC machine applications. Please see the docs.
88

99
import uasyncio as asyncio
1010
from machine import Pin
1111

1212
class Encoder:
13-
LATENCY = 50
13+
delay = 100 # Pause (ms) for motion to stop
1414

15-
def __init__(self, pin_x, pin_y, v=0, vmin=None, vmax=None,
15+
def __init__(self, pin_x, pin_y, v=0, vmin=None, vmax=None, div=1,
1616
callback=lambda a, b : None, args=()):
1717
self._pin_x = pin_x
1818
self._pin_y = pin_y
19-
self._v = v
19+
self._v = 0 # Hardware value always starts at 0
20+
self._cv = v # Current (divided) value
21+
if ((vmin is not None) and v < min) or ((vmax is not None) and v > vmax):
22+
raise ValueError('Incompatible args: must have vmin <= v <= vmax')
2023
self._tsf = asyncio.ThreadSafeFlag()
24+
trig = Pin.IRQ_RISING | Pin.IRQ_FALLING
2125
try:
22-
xirq = pin_x.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=self._x_cb, hard=True)
23-
yirq = pin_y.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=self._y_cb, hard=True)
24-
except TypeError:
25-
xirq = pin_x.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=self._x_cb)
26-
yirq = pin_y.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=self._y_cb)
27-
asyncio.create_task(self._run(vmin, vmax, callback, args))
28-
26+
xirq = pin_x.irq(trigger=trig, handler=self._x_cb, hard=True)
27+
yirq = pin_y.irq(trigger=trig, handler=self._y_cb, hard=True)
28+
except TypeError: # hard arg is unsupported on some hosts
29+
xirq = pin_x.irq(trigger=trig, handler=self._x_cb)
30+
yirq = pin_y.irq(trigger=trig, handler=self._y_cb)
31+
asyncio.create_task(self._run(vmin, vmax, div, callback, args))
2932

3033
# Hardware IRQ's
3134
def _x_cb(self, pin):
@@ -38,21 +41,32 @@ def _y_cb(self, pin):
3841
self._v += 1 if fwd else -1
3942
self._tsf.set()
4043

41-
async def _run(self, vmin, vmax, cb, args):
42-
pv = self._v # Prior value
44+
async def _run(self, vmin, vmax, div, cb, args):
45+
pv = self._v # Prior hardware value
46+
cv = self._cv # Current divided value as passed to callback
47+
pcv = cv # Prior divided value passed to callback
48+
mod = 0
49+
delay = self.delay
4350
while True:
4451
await self._tsf.wait()
45-
cv = self._v # Current value
52+
await asyncio.sleep_ms(delay) # Wait for motion to stop
53+
new = self._v # Sample hardware (atomic read)
54+
a = new - pv # Hardware change
55+
# Ensure symmetrical bahaviour for + and - values
56+
q, r = divmod(abs(a), div)
57+
if a < 0:
58+
r = -r
59+
q = -q
60+
pv = new - r # Hardware value when local value was updated
61+
cv += q
4662
if vmax is not None:
4763
cv = min(cv, vmax)
4864
if vmin is not None:
4965
cv = max(cv, vmin)
50-
self._v = cv
51-
#print(cv, pv)
52-
if cv != pv:
53-
cb(cv, cv - pv, *args) # User CB in uasyncio context
54-
pv = cv
55-
await asyncio.sleep_ms(self.LATENCY)
66+
self._cv = cv # For value()
67+
if cv != pcv:
68+
cb(cv, cv - pcv, *args) # User CB in uasyncio context
69+
pcv = cv
5670

5771
def value(self):
58-
return self._v
72+
return self._cv

0 commit comments

Comments
 (0)