2021-07-04 17:21:37 +00:00
|
|
|
# encoder.py Asynchronous driver for incremental quadrature encoder.
|
|
|
|
|
2022-04-17 11:09:28 +00:00
|
|
|
# Copyright (c) 2021-2022 Peter Hinch
|
2021-07-04 17:21:37 +00:00
|
|
|
# Released under the MIT License (MIT) - see LICENSE file
|
|
|
|
|
2022-04-21 16:55:12 +00:00
|
|
|
# Thanks are due to @ilium007 for identifying the issue of tracking detents,
|
|
|
|
# https://github.com/peterhinch/micropython-async/issues/82.
|
|
|
|
# Also to Mike Teachman (@miketeachman) for design discussions and testing
|
|
|
|
# against a state table design
|
|
|
|
# https://github.com/miketeachman/micropython-rotary/blob/master/rotary.py
|
|
|
|
|
2021-07-04 17:21:37 +00:00
|
|
|
import uasyncio as asyncio
|
|
|
|
from machine import Pin
|
2023-11-02 15:15:41 +00:00
|
|
|
from . import Delay_ms
|
2021-07-04 17:21:37 +00:00
|
|
|
|
|
|
|
|
2023-11-02 15:15:41 +00:00
|
|
|
class Encoder:
|
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
pin_x,
|
|
|
|
pin_y,
|
|
|
|
v=0,
|
|
|
|
div=1,
|
|
|
|
vmin=None,
|
|
|
|
vmax=None,
|
|
|
|
mod=None,
|
|
|
|
callback=lambda a, b: None,
|
|
|
|
args=(),
|
|
|
|
delay=100,
|
|
|
|
):
|
2021-07-04 17:21:37 +00:00
|
|
|
self._pin_x = pin_x
|
|
|
|
self._pin_y = pin_y
|
2022-04-17 11:09:28 +00:00
|
|
|
self._x = pin_x()
|
|
|
|
self._y = pin_y()
|
2022-04-21 12:50:32 +00:00
|
|
|
self._v = v * div # Initialise hardware value
|
2021-07-04 17:21:37 +00:00
|
|
|
self._cv = v # Current (divided) value
|
2023-11-02 15:15:41 +00:00
|
|
|
self._delay = Delay_ms(duration=delay)
|
|
|
|
self._timeout = 2 * delay # Continuous rotation timeout
|
|
|
|
# Pause (ms) for motion to stop/limit callback frequency
|
2022-06-08 16:10:50 +00:00
|
|
|
|
2022-04-17 11:09:28 +00:00
|
|
|
if ((vmin is not None) and v < vmin) or ((vmax is not None) and v > vmax):
|
2023-11-02 15:15:41 +00:00
|
|
|
raise ValueError("Incompatible args: must have vmin <= v <= vmax")
|
2021-07-04 17:21:37 +00:00
|
|
|
self._tsf = asyncio.ThreadSafeFlag()
|
|
|
|
trig = Pin.IRQ_RISING | Pin.IRQ_FALLING
|
|
|
|
try:
|
|
|
|
xirq = pin_x.irq(trigger=trig, handler=self._x_cb, hard=True)
|
|
|
|
yirq = pin_y.irq(trigger=trig, handler=self._y_cb, hard=True)
|
|
|
|
except TypeError: # hard arg is unsupported on some hosts
|
|
|
|
xirq = pin_x.irq(trigger=trig, handler=self._x_cb)
|
|
|
|
yirq = pin_y.irq(trigger=trig, handler=self._y_cb)
|
2023-11-02 15:15:41 +00:00
|
|
|
self._runt = asyncio.create_task(self._run(vmin, vmax, div, mod, callback, args))
|
2021-07-04 17:21:37 +00:00
|
|
|
|
2022-04-17 13:42:23 +00:00
|
|
|
# Hardware IRQ's. Duration 36μs on Pyboard 1 ~50μs on ESP32.
|
|
|
|
# IRQ latency: 2nd edge may have occured by the time ISR runs, in
|
|
|
|
# which case there is no movement.
|
2022-04-17 11:09:28 +00:00
|
|
|
def _x_cb(self, pin_x):
|
2022-04-17 13:42:23 +00:00
|
|
|
if (x := pin_x()) != self._x:
|
|
|
|
self._x = x
|
|
|
|
self._v += 1 if x ^ self._pin_y() else -1
|
|
|
|
self._tsf.set()
|
2021-07-04 17:21:37 +00:00
|
|
|
|
2022-04-17 11:09:28 +00:00
|
|
|
def _y_cb(self, pin_y):
|
2022-04-17 13:42:23 +00:00
|
|
|
if (y := pin_y()) != self._y:
|
|
|
|
self._y = y
|
2022-04-17 17:02:38 +00:00
|
|
|
self._v -= 1 if y ^ self._pin_x() else -1
|
2022-04-17 13:42:23 +00:00
|
|
|
self._tsf.set()
|
2021-07-04 17:21:37 +00:00
|
|
|
|
2023-11-02 15:15:41 +00:00
|
|
|
async def respond(self): # Retrigger the delay until motion/bounce stops
|
|
|
|
while True:
|
|
|
|
await self._tsf.wait()
|
|
|
|
self._delay.trigger()
|
|
|
|
|
2022-04-21 12:50:32 +00:00
|
|
|
async def _run(self, vmin, vmax, div, mod, cb, args):
|
2021-07-04 17:21:37 +00:00
|
|
|
pv = self._v # Prior hardware value
|
2022-04-21 12:50:32 +00:00
|
|
|
pcv = self._cv # Prior divided value passed to callback
|
|
|
|
lcv = pcv # Current value after limits applied
|
|
|
|
plcv = pcv # Previous value after limits applied
|
2023-11-02 15:15:41 +00:00
|
|
|
self._rspt = asyncio.create_task(self.respond())
|
2021-07-04 17:21:37 +00:00
|
|
|
while True:
|
2023-11-02 15:15:41 +00:00
|
|
|
try:
|
|
|
|
await asyncio.wait_for_ms(self._delay.wait(), self._timeout)
|
|
|
|
except asyncio.TimeoutError: # Continuous rotation
|
|
|
|
pass
|
|
|
|
self._delay.clear()
|
2022-04-21 12:50:32 +00:00
|
|
|
hv = self._v # Sample hardware (atomic read).
|
|
|
|
if hv == pv: # A change happened but was negated before
|
|
|
|
continue # this got scheduled. Nothing to do.
|
|
|
|
pv = hv
|
|
|
|
cv = round(hv / div) # cv is divided value.
|
|
|
|
if not (dv := cv - pcv): # dv is change in divided value.
|
|
|
|
continue # No change
|
|
|
|
lcv += dv # lcv: divided value with limits/mod applied
|
|
|
|
lcv = lcv if vmax is None else min(vmax, lcv)
|
|
|
|
lcv = lcv if vmin is None else max(vmin, lcv)
|
|
|
|
lcv = lcv if mod is None else lcv % mod
|
|
|
|
self._cv = lcv # update ._cv for .value() before CB.
|
|
|
|
if lcv != plcv:
|
|
|
|
cb(lcv, lcv - plcv, *args) # Run user CB in uasyncio context
|
2021-07-04 17:21:37 +00:00
|
|
|
pcv = cv
|
2022-04-21 12:50:32 +00:00
|
|
|
plcv = lcv
|
2021-07-04 17:21:37 +00:00
|
|
|
|
|
|
|
def value(self):
|
|
|
|
return self._cv
|
2023-11-02 15:15:41 +00:00
|
|
|
|
|
|
|
def deinit(self):
|
|
|
|
self._rspt.cancel()
|
|
|
|
self._runt.cancel()
|