First off, I agree with @thephez that this question is probably better for stackoverflow. Since there is a overlap with the RPI.GPIO
, I have taken the liberty to answer it the best I can.
Part 1 - RPI.GPIO
To really understand how the callback mechanism is implemented/works in RPI.GPIO, I dug into its
source code
Within it there is a file event_gpio.c
and between lines 430 through 480 I found
the definition of the function add_edge_detect
Within this function a background
thread is launched using pthread_create
which essentially blocks until some
interesting GPIO pin transition happens. When such an event occurs, the thread
unblocks and executes the collection of callbacks registered.
int add_edge_detect(unsigned int gpio, unsigned int edge, int bouncetime)
// return values:
// 0 - Success
// 1 - Edge detection already added
// 2 - Other error
{
// <<< snip >>>
// start poll thread if it is not already running
if (!thread_running) {
if (pthread_create(&threads, NULL, poll_thread, (void *)t) != 0) {
remove_edge_detect(gpio);
return 2;
}
}
return 0;
}
Part 2 - Python asyncio
Consider the simple async example here
import asyncio
def hello_world(loop):
print('Hello World')
loop.stop()
loop = asyncio.get_event_loop()
# Schedule a call to hello_world()
loop.call_soon(hello_world, loop)
# Blocking call interrupted by loop.stop()
loop.run_forever()
loop.close()
When a python process starts, there is by default 1 thread that executes the python code.
In the above example one can see there is a blocking call to loop.run_forever()
.
Internally it would wait for some task
to be submitted (execute hello_world
in this case)
into a priority queue and execute accordingly.
To verify this, we need to peek a bit under the hood. From the source code of loop.run_forever
in base_events.py
one can see it simply keeps calling _run_once()
in a loop
def run_forever(self):
"""Run until stop() is called."""
self._check_closed()
if self.is_running():
raise RuntimeError('Event loop is running.')
self._thread_id = threading.get_ident()
try:
while True:
try:
self._run_once()
except _StopError:
break
finally:
self._thread_id = None
_run_once
is implemented like so using a heap (priority queue)
def _run_once(self):
"""Run one full iteration of the event loop.
This calls all currently ready callbacks, polls for I/O,
schedules the resulting callbacks, and finally schedules
'call_later' callbacks.
"""
sched_count = len(self._scheduled)
if (sched_count > _MIN_SCHEDULED_TIMER_HANDLES and
self._timer_cancelled_count / sched_count >
_MIN_CANCELLED_TIMER_HANDLES_FRACTION):
# Remove delayed calls that were cancelled if their number
# is too high
new_scheduled = []
for handle in self._scheduled:
if handle._cancelled:
handle._scheduled = False
else:
new_scheduled.append(handle)
heapq.heapify(new_scheduled)
self._scheduled = new_scheduled
self._timer_cancelled_count = 0
else:
# Remove delayed calls that were cancelled from head of queue.
while self._scheduled and self._scheduled[0]._cancelled:
self._timer_cancelled_count -= 1
handle = heapq.heappop(self._scheduled)
handle._scheduled = False
timeout = None
if self._ready:
timeout = 0
elif self._scheduled:
# Compute the desired timeout.
when = self._scheduled[0]._when
timeout = max(0, when - self.time())
if self._debug and timeout != 0:
t0 = self.time()
event_list = self._selector.select(timeout)
dt = self.time() - t0
if dt >= 1.0:
level = logging.INFO
else:
level = logging.DEBUG
nevent = len(event_list)
if timeout is None:
logger.log(level, 'poll took %.3f ms: %s events',
dt * 1e3, nevent)
elif nevent:
logger.log(level,
'poll %.3f ms took %.3f ms: %s events',
timeout * 1e3, dt * 1e3, nevent)
elif dt >= 1.0:
logger.log(level,
'poll %.3f ms took %.3f ms: timeout',
timeout * 1e3, dt * 1e3)
else:
event_list = self._selector.select(timeout)
self._process_events(event_list)
# Handle 'later' callbacks that are ready.
end_time = self.time() + self._clock_resolution
while self._scheduled:
handle = self._scheduled[0]
if handle._when >= end_time:
break
handle = heapq.heappop(self._scheduled)
handle._scheduled = False
self._ready.append(handle)
# This is the only place where callbacks are actually *called*.
# All other places just add them to ready.
# Note: We run all currently scheduled callbacks, but not any
# callbacks scheduled by callbacks run this time around --
# they will be run the next time (after another I/O poll).
# Use an idiom that is thread-safe without using locks.
ntodo = len(self._ready)
for i in range(ntodo):
handle = self._ready.popleft()
if handle._cancelled:
continue
if self._debug:
try:
self._current_handle = handle
t0 = self.time()
handle._run()
dt = self.time() - t0
if dt >= self.slow_callback_duration:
logger.warning('Executing %s took %.3f seconds',
_format_handle(handle), dt)
finally:
self._current_handle = None
else:
handle._run()
handle = None # Needed to break cycles when an exception occurs.
Part 3
Explaining the error you are getting
RuntimeError: There is no current event loop in thread 'Dummy-4'.
This would be because the thread 'Dummy-4' is the thread described in Part 1 which
executes the GPIO callbacks. As there is no loop
object created on this thread;
there is no loop.run_forever()
that is required to service your message_manager_f
callback
Part 4
How to fix this problem ?
Simply introducing a event loop within your def motion_sensor
is not
the right way to do it
def motion_sensor(self, message_manager_f):
loop = asyncio.get_event_loop()
loop.call_soon(message_manager_f, loop)
loop.run_forever()
loop.close()
This wrong is because
- the call wouldnt really be async with respect to the GPIO callback thread
- this will block the RPI.GPIO's thread from servicing other callbacks
My solution would look something like this
import asyncio
import RPIO.GPIO as GPIO
import sys
loop = None
def message_manager_f():
print ":P message_manager_f()"
def motion_sensor(self, message_manager_f):
if loop is None:
print(":(")
return # should not come to this
# this enqueues a call to message_manager_f()
loop.call_soon_threadsafe(message_manager_f)
# this is the primary thread mentioned in Part 2
if __name__ == '__main__':
try:
# setup the GPIO
GPIO.setwarnings(True)
GPIO.setmode(GPIO.BCM)
GPIO.setup(4, GPIO.IN) # adjust the PULL UP/PULL DOWN as applicable
GPIO.add_event_detect(4, GPIO.RISING, callback=lambda x: self.motion_sensor(message_manager_f), bouncetime=500)
# run the event loop
loop = asyncio.get_event_loop()
loop.run_forever()
loop.close()
except :
print("Error:", sys.exc_info()[0])
# cleanup
GPIO.cleanup()
References
Notes
- As I don't have the required h/w handy, I haven't had a chance to verify the above code works correctly or not.
- Any errors or questions, please leave a comment and I will try to address it the best I can