Reconnect and keep reading from device with asyncio and BleakClient - python

This is a question on how to reach a certain goal, but also for help explaining some behaviors I don't understand since I'm quite new to both bleak and async programming in python.
I have a Bluetooth device that sends data and the goal is to read from it and in case it turns off, wait for it to turn back on again and keep reading. But I'm having problems with that last part.
Here's what I have so far (the function that does it all, I just call it with asyncio.run(read_data())):
async def read_data():
def handle_disconnect(_: BleakClient):
print("Device was disconnected, goodbye.")
def handle_response(_: int, data: bytearray):
print("received:", data)
while True:
device = await BleakScanner.find_device_by_filter(
lambda d, ad: d.name and d.name.lower() == GLOVEName.lower()
)
try:
print(vars(device))
except:
print(device)
if device is None:
await asyncio.sleep(1)
continue
client = BleakClient(device, disconnected_callback=handle_disconnect)
print(client.is_connected)
try:
await client.connect()
await client.start_notify(UART_TX_CHAR_UUID, handle_response)
except BleakError as e:
print(e)
await client.disconnect()
Here's the prints of an execution with a device disconnection in between:
{'address': 'F4:1D:97:AD:FE:39', 'name': 'GLOVE-fe39', 'details': _RawAdvData(adv=<_bleak_winrt_Windows_Devices_Bluetooth_Advertisement.BluetoothLEAdvertisementReceivedEventArgs object at 0x00000290950A8490>, scan=<_bleak_winrt_Windows_Devices_Bluetooth_Advertisement.BluetoothLEAdvertisementReceivedEventArgs object at 0x00000290950A8310>), 'rssi': -52, 'metadata': {'uuids': [], 'manufacturer_data': {89: b'\x19\x1a\x1b\x1c\x1d', 4660: b'\x01\x02\x03\x04\x05\x06\x07\x08'}}}
False
received: bytearray(b'fe39,1,281,293,312,316,293,272,315,314')
received: bytearray(b'fe39,2,290,297,312,316,296,276,307,308')
received: bytearray(b'fe39,3,295,300,313,318,298,277,302,301')
received: bytearray(b'fe39,4,279,284,292,291,276,259,282,284')
received: bytearray(b'fe39,5,263,266,266,263,249,236,254,259')
received: bytearray(b'fe39,6,279,284,292,293,276,259,277,272')
received: bytearray(b'fe39,7,244,242,236,234,219,210,228,232')
received: bytearray(b'fe39,8,224,224,207,208,192,183,201,208')
received: bytearray(b'fe39,9,228,225,216,213,195,191,207,208')
received: bytearray(b'fe39,10,202,204,187,182,172,168,183,192')
None
None
received: bytearray(b'fe39,2,290,297,312,316,296,276,307,308')
received: bytearray(b'fe39,3,295,300,313,318,298,277,302,301')
received: bytearray(b'fe39,4,279,284,292,291,276,259,282,284')
received: bytearray(b'fe39,5,263,266,266,263,249,236,254,259')
received: bytearray(b'fe39,6,279,284,292,293,276,259,277,272')
received: bytearray(b'fe39,7,244,242,236,234,219,210,228,232')
received: bytearray(b'fe39,8,224,224,207,208,192,183,201,208')
received: bytearray(b'fe39,9,228,225,216,213,195,191,207,208')
None
Device was disconnected, goodbye.
None
{'address': 'F4:1D:97:AD:FE:39', 'name': 'GLOVE-fe39', 'details': _RawAdvData(adv=<_bleak_winrt_Windows_Devices_Bluetooth_Advertisement.BluetoothLEAdvertisementReceivedEventArgs object at 0x00000290951D73F0>, scan=<_bleak_winrt_Windows_Devices_Bluetooth_Advertisement.BluetoothLEAdvertisementReceivedEventArgs object at 0x00000290951D7030>), 'rssi': -63, 'metadata': {'uuids': [], 'manufacturer_data': {89: b'\x19\x1a\x1b\x1c\x1d', 4660: b'\x01\x02\x03\x04\x05\x06\x07\x08'}}}
False
Traceback (most recent call last):
File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.9_3.9.3568.0_x64__qbz5n2kfra8p0\lib\asyncio\locks.py", line 226, in wait
await fut
asyncio.exceptions.CancelledError
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "C:\Users\tomas\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.9_qbz5n2kfra8p0\LocalCache\local-packages\Python39\site-packages\bleak\backends\winrt\client.py", line 380, in connect
await asyncio.gather(wait_connect_task, wait_get_services_task)
asyncio.exceptions.CancelledError
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "C:\Users\tomas\Documents\Glooma\py_files\uart_service.py", line 110, in <module>
asyncio.run(uart_terminal())
File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.9_3.9.3568.0_x64__qbz5n2kfra8p0\lib\asyncio\runners.py", line 44, in run
return loop.run_until_complete(main)
File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.9_3.9.3568.0_x64__qbz5n2kfra8p0\lib\asyncio\base_events.py", line 647, in run_until_complete
return future.result()
File "C:\Users\tomas\Documents\Glooma\py_files\uart_service.py", line 76, in uart_terminal
await client.connect()
File "C:\Users\tomas\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.9_qbz5n2kfra8p0\LocalCache\local-packages\Python39\site-packages\bleak\__init__.py", line 471, in connect
return await self._backend.connect(**kwargs)
File "C:\Users\tomas\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.9_qbz5n2kfra8p0\LocalCache\local-packages\Python39\site-packages\bleak\backends\winrt\client.py", line 380, in connect
await asyncio.gather(wait_connect_task, wait_get_services_task)
File "C:\Users\tomas\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.9_qbz5n2kfra8p0\LocalCache\local-packages\Python39\site-packages\async_timeout\__init__.py", line 129, in __aexit__
self._do_exit(exc_type)
File "C:\Users\tomas\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.9_qbz5n2kfra8p0\LocalCache\local-packages\Python39\site-packages\async_timeout\__init__.py", line 212, in _do_exit
raise asyncio.TimeoutError
asyncio.exceptions.TimeoutError
So, first it connects well, and then comes my first question, why does the client.is_connected return false if it is connected?
I then press the button on the device which sends data and gets printed in the terminal, I then press the button again and it stops. So far so good.
I can keep pressing the button as many times as I like and it will keep starting and stopping sending and printing data which is the intended behaviour. Another question I have is that it seems like there's two things happening here, the loop checking for the device and then printing None because since it was already connected the Scanner can't find it, which goes inside the if clause, waits a second and starts again. But at the same time the notifications are going through and getting printed. Seems like two threads going on printing on the terminal, which I don't understand. I assume it is related to asynchronous programming.
After that I turn off the device and turn it back on. The device is found again going by the prints, again with a False in the is_connected. But this second time it fails with a time out error in await client.connect(). Why? What am I doing wrong here?
I've tried adding some code to run the await client.disconnect when I turn off the device, before connecting again, and that doesn't throw the timeout error on await client.connect(), but it just stays in the loop printing None and doesn't read anything when the device sends data.
Any help either explaining this behavior or with a solution to the problem would be appreciated.
Thanks.

Related

Could someone help me find the error in this code?

Here is my code:
from discord.ext import commands
import os
import discord
import requests
import json
my_secret = os.environ['MY_TOKEN']
GUILD = os.getenv('DISCORD_GUILD')
bot = commands.Bot(command_prefix='!') #prefix
discord.CustomActivity("Jaxon\'s brain melt", ActivityType="watching", emoji=None)
#bot.command(name="enfr", help="Translates text from english to french")
async def enfr(ctx, text):
response = requests.get("https://api.mymemory.translated.net/get?q=" + text + "&langpair=en|fr")
json_data = json.loads(response.text)
translation = json_data.get("translatedText") + "test"
print(response.status_code)
await ctx.channel.send(translation)
bot.run(my_secret)
This is the error I get:
200
Ignoring exception in command enfr:
Traceback (most recent call last):
File "/home/runner/translate-Bot/venv/lib/python3.8/site-packages/discord/ext/commands/core.py", line 85, in wrapped
ret = await coro(*args, **kwargs)
File "main.py", line 22, in enfr
await ctx.channel.send(translation)
File "/home/runner/translate-Bot/venv/lib/python3.8/site-packages/discord/abc.py", line 1065, in send
data = await state.http.send_message(channel.id, content, tts=tts, embed=embed,
File "/home/runner/translate-Bot/venv/lib/python3.8/site-packages/discord/http.py", line 254, in request
raise HTTPException(r, data)
discord.errors.HTTPException: 400 Bad Request (error code: 50006): Cannot send an empty message
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/home/runner/translate-Bot/venv/lib/python3.8/site-packages/discord/ext/commands/bot.py", line 939, in invoke
await ctx.command.invoke(ctx)
File "/home/runner/translate-Bot/venv/lib/python3.8/site-packages/discord/ext/commands/core.py", line 863, in invoke
await injected(*ctx.args, **ctx.kwargs)
File "/home/runner/translate-Bot/venv/lib/python3.8/site-packages/discord/ext/commands/core.py", line 94, in wrapped
raise CommandInvokeError(exc) from exc
discord.ext.commands.errors.CommandInvokeError: Command raised an exception: HTTPException: 400 Bad Request (error code: 50006): Cannot send an empty message
It worked a few days ago and now it doesn't :(
There is one other command, but it doesn't don't affect this one so I removed it from the code to clear it up a bit. I get that error when I try to run the command !enfr hello
I don't know if this is the correct place to ask, my last questions were disliked by some people.
The traceback specifies the error is Cannot send an empty message. Specifically:
discord.ext.commands.errors.CommandInvokeError: Command raised an exception: HTTPException: 400 Bad Request (error code: 50006): Cannot send an empty message
Looking at it more closely you can find the line of your file where the Exception is being thrown.
File "main.py", line 22, in enfr await ctx.channel.send(translation)
From these it seems quite clear that translation is None or an empty string, and that the function ctx.channel.send throws an Exception when that is the case.
Now try to figure out why translation is empty by reviewing how you're creating it in the first place, and account for these edge-cases.
Note: Reading traceback logs might seem daunting at first, but they're quite informative (at worst showing you where in your code the issue is). Don't panic and just go through them slowly :)

Cannot close a running event loop

What is the way to close the discord.py bot loop once tasks are done?
Added:
nest_asyncio.apply()
Tried:
bot.logout(), bot.close(), bot.cancel()
Code:
async def helper(bot, discord_user_feed):
nest_asyncio.apply()
await bot.wait_until_ready()
# await asyncio.sleep(10)
for each_id, events_list in discord_user_feed.items():
discord_user = await bot.fetch_user(int(each_id))
for each_one in events_list:
msg_sent = True
while msg_sent:
await discord_user.send(each_one)
msg_sent = False
await bot.close()
return 'discord messages sent'
async def discord_headlines(request):
nest_asyncio.apply()
discord_user_feed = await get_details()
bot = commands.Bot(command_prefix='!')
bot.loop.create_task(helper(bot, discord_user_feed))
message = bot.run('my_token')
return HttpResponse(message)
I can be able to send messages to discord users using id and discord bot. But, even after, django view api is continuously running and getting the error. It needs to return a response message - discord messages sent.
Error:
Exception in callback <TaskWakeupMethWrapper object at 0x000001C852D2DA38>(<Future finis...SWdZVtXT7E']}>)
handle: <Handle <TaskWakeupMethWrapper object at 0x000001C852D2DA38>(<Future finis...SWdZVtXT7E']}>)>
Traceback (most recent call last):
File "E:\sd\envs\port\lib\asyncio\events.py", line 145, in _run
self._callback(*self._args)
KeyError: <_WindowsSelectorEventLoop running=True closed=False debug=False>
Internal Server Error: /api/v0/discord_headlines/
Traceback (most recent call last):
File "E:\sd\envs\port\lib\site-packages\django\core\handlers\exception.py", line 47, in inner
response = get_response(request)
File "E:\sd\envs\port\lib\site-packages\django\core\handlers\base.py", line 179, in _get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "E:\sd\envs\port\lib\site-packages\asgiref\sync.py", line 139, in __call__
return call_result.result()
File "E:\sd\envs\port\lib\concurrent\futures\_base.py", line 425, in result
return self.__get_result()
File "E:\sd\envs\port\lib\concurrent\futures\_base.py", line 384, in __get_result
raise self._exception
File "E:\sd\envs\port\lib\site-packages\asgiref\sync.py", line 204, in main_wrap
result = await self.awaitable(*args, **kwargs)
File "E:\back-end\port_api\views\discord_integration.py", line 110, in discord_headlines
message = bot.run('my_token')
File "E:\sd\envs\port\lib\site-packages\discord\client.py", line 719, in run
_cleanup_loop(loop)
File "E:\sd\envs\port\lib\site-packages\discord\client.py", line 95, in _cleanup_loop
loop.close()
File "E:\sd\envs\port\lib\asyncio\selector_events.py", line 107, in close
raise RuntimeError("Cannot close a running event loop")
RuntimeError: Cannot close a running event loop
[13/Jun/2021 01:07:34] "GET /api/v0/discord_headlines/ HTTP/1.1" 500 109010
These are the versions:
Python 3.6.4, Django 3.1, discord.py 1.7.3, Asyncio, Anaconda-4.9.2, Windows-10
I've not used older versions so I can't attest to the accuracy of this answer for pre-2021 releases (v1.6.0+) Discord.py.
When starting a discord Client or Bot via run(*args, **kwargs) there is actually no class method to stop it.
Per the Discord.py Documentations Discord.py discord.Client.run:
A blocking call that abstracts away the event loop initialisation from
you.
If you want more control over the event loop then this function should
not be used. Use start() coroutine or connect() + login()
And it's effectively equal to:
# run(*args, **kwargs) is basically
try:
# Start the bot on asyncio event loop with using .start() coroutine
loop.run_until_complete(start(*args, **kwargs))
except KeyboardInterrupt:
# Invoke close() coroutine on the same loop if interrupt
loop.run_until_complete(close())
# Cancel any other lingering tasks and such
finally:
loop.close()
So the unrewarding but accurate answer with your current attempted implementation is simply... you can't.
If you wanna drop some more info on the webserver library your using in response or via a message, I can help craft you something functional.
If your webserver is fully asyncio integrated, your best bet is seeing if the library has a way to add tasks to its existing event loop, or if you pass one to it. Sanic allows for this in its decorator startup/shutdown methods (ex. after_server_start, etc). Store the reference to the bot instance somewhere and pass it around as needed.
I'll try and keep an eye on here for a response or DM and try to help get you functional and we can post updated code answer here, there's a big lack of solid answers for using discord.py in more complex cases within other applications/webservices and I just ran into a similiar issue with writing a discord.py bot that accepted webhook data and posted embeds to appropriate channels based on some filters.

Django Channels Redis: Exception inside application: Lock is not acquired

Fully loaded multi-tenant Django application with 1000's of WebSockets using Daphne/Channels, running fine for a few months and suddenly tenants all calling it the support line the application running slow or outright hanging. Narrowed it down to WebSockets as HTTP REST API hits came through fast and error free.
None of the application logs or OS logs indicate some issue, so only thing to go on is the exception noted below. It happened over and over again here and there throughout 2 days.
I don't expect any deep debugging help, just some off-the-cuff advice on possibilities.
AWS Linux 1
Python 3.6.4
Elasticache Redis 5.0
channels==2.4.0
channels-redis==2.4.2
daphne==2.5.0
Django==2.2.13
Split configuration HTTP served by uwsgi, daphne serves asgi, Nginx
May 10 08:08:16 prod-b-web1: [pid 15053] [version 119.5.10.5086] [tenant_id -] [domain_name -] [pathname /opt/releases/r119.5.10.5086/env/lib/python3.6/site-packages/daphne/server.py] [lineno 288] [priority ERROR] [funcname application_checker] [request_path -] [request_method -] [request_data -] [request_user -] [request_stack -] Exception inside application: Lock is not acquired.
Traceback (most recent call last):
File "/opt/releases/r119.5.10.5086/env/lib/python3.6/site-packages/channels_redis/core.py", line 435, in receive
real_channel
File "/opt/releases/r119.5.10.5086/env/lib/python3.6/site-packages/channels_redis/core.py", line 484, in receive_single
await self.receive_clean_locks.acquire(channel_key)
File "/opt/releases/r119.5.10.5086/env/lib/python3.6/site-packages/channels_redis/core.py", line 152, in acquire
return await self.locks[channel].acquire()
File "/opt/python3.6/lib/python3.6/asyncio/locks.py", line 176, in acquire
yield from fut
concurrent.futures._base.CancelledError
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/opt/releases/r119.5.10.5086/env/lib/python3.6/site-packages/channels/sessions.py", line 183, in __call__
return await self.inner(receive, self.send)
File "/opt/releases/r119.5.10.5086/env/lib/python3.6/site-packages/channels/middleware.py", line 41, in coroutine_call
await inner_instance(receive, send)
File "/opt/releases/r119.5.10.5086/env/lib/python3.6/site-packages/channels/consumer.py", line 59, in __call__
[receive, self.channel_receive], self.dispatch
File "/opt/releases/r119.5.10.5086/env/lib/python3.6/site-packages/channels/utils.py", line 58, in await_many_dispatch
await task
File "/opt/releases/r119.5.10.5086/env/lib/python3.6/site-packages/channels_redis/core.py", line 447, in receive
self.receive_lock.release()
File "/opt/python3.6/lib/python3.6/asyncio/locks.py", line 201, in release
raise RuntimeError('Lock is not acquired.')
RuntimeError: Lock is not acquired.
First, lets have a look at the source of the RuntimeError: Lock is not acquired. error. As given by the traceback, the release() method in the file /opt/python3.6/lib/python3.6/asyncio/locks.py is defined like so:
def release(self):
"""Release a lock.
When the lock is locked, reset it to unlocked, and return.
If any other coroutines are blocked waiting for the lock to become
unlocked, allow exactly one of them to proceed.
When invoked on an unlocked lock, a RuntimeError is raised.
There is no return value.
"""
if self._locked:
self._locked = False
self._wake_up_first()
else:
raise RuntimeError('Lock is not acquired.')
A primitive lock is a synchronization primitive that is not owned by a particular thread when locked.
When attempting to release an unlocked lock by calling the release() method, the RuntimeError will be raised, as the method should only be called in the locked state. The state changes to unlocked when called in the locked state.
Now for the previous error raised in the acquire() method in the same file, the acquire() method is defined like so:
async def acquire(self):
"""Acquire a lock.
This method blocks until the lock is unlocked, then sets it to
locked and returns True.
"""
if (not self._locked and (self._waiters is None or
all(w.cancelled() for w in self._waiters))):
self._locked = True
return True
if self._waiters is None:
self._waiters = collections.deque()
fut = self._loop.create_future()
self._waiters.append(fut)
# Finally block should be called before the CancelledError
# handling as we don't want CancelledError to call
# _wake_up_first() and attempt to wake up itself.
try:
try:
await fut
finally:
self._waiters.remove(fut)
except exceptions.CancelledError:
if not self._locked:
self._wake_up_first()
raise
self._locked = True
return True
So in order for the concurrent.futures._base.CancelledError error you're getting to be raised, the await fut must've caused the issue.
To fix it, you can have a look at Awaiting an asyncio.Future raises concurrent.futures._base.CancelledError instead of waiting for a value/exception to be set
Basically, you might have an awaitable in your code that you didn't await, and by not awaiting it, you never handed control back to the event loop or store the awaitable, causing it to be immediately cleaned up, completely cancelling it (and all of the awaitables it controlled).
Simply make sure you await the results of the awaitables in your code, finding any you missed.

When running bot sample code, I get this error

My code is exactly
This
(With my token of course)
When I run it, my bot starts up as normal, but when a new person is added to the server, i get this.
------
Ignoring exception in on_member_join
Traceback (most recent call last):
File "C:\Users\USRNAME\AppData\Local\Programs\Python\Python36\lib\site-packages\discord\client.py", line 307, in _run_event
yield from getattr(self, event)(*args, **kwargs)
File "test.py", line 9, in on_member_join
await client.send_message(server, fmt.format(member, server))
File "C:\Users\USRNAME\AppData\Local\Programs\Python\Python36\lib\site-packages\discord\client.py", line 1152, in send_message
data = yield from self.http.send_message(channel_id, content, guild_id=guild_id, tts=tts, embed=embed)
File "C:\Users\USRNAME\AppData\Local\Programs\Python\Python36\lib\site-packages\discord\http.py", line 198, in request
raise NotFound(r, data)
discord.errors.NotFound: NOT FOUND (status code: 404): Unknown Channel
(Sorry about it not being in a code block, I'm new to stack exchange)
Any help would be appreciated. Thanks
This will not work anymore because discord removed default channels so sending it to server will not work. You should replace server with discord.Object('insert channel id') if you are using async or discord.Object(insert channel id) if you are using the rewrite branch. Note the string vs int difference. Good luck :)

Google PubSub python client returning StatusCode.UNAVAILABLE

I am trying to establish a long running Pull subscription to a Google Cloud PubSub topic.
I am using a code very similar to the example given in the documentation here, i.e.:
def receive_messages(project, subscription_name):
"""Receives messages from a pull subscription."""
subscriber = pubsub_v1.SubscriberClient()
subscription_path = subscriber.subscription_path(
project, subscription_name)
def callback(message):
print('Received message: {}'.format(message))
message.ack()
subscriber.subscribe(subscription_path, callback=callback)
# The subscriber is non-blocking, so we must keep the main thread from
# exiting to allow it to process messages in the background.
print('Listening for messages on {}'.format(subscription_path))
while True:
time.sleep(60)
The problem is that I'm receiving the following traceback sometimes:
Exception in thread Consumer helper: consume bidirectional stream:
Traceback (most recent call last):
File "/usr/lib/python3.5/threading.py", line 914, in _bootstrap_inner
self.run()
File "/usr/lib/python3.5/threading.py", line 862, in run
self._target(*self._args, **self._kwargs)
File "/path/to/google/cloud/pubsub_v1/subscriber/_consumer.py", line 248, in _blocking_consume
self._policy.on_exception(exc)
File "/path/to/google/cloud/pubsub_v1/subscriber/policy/thread.py", line 135, in on_exception
raise exception
File "/path/to/google/cloud/pubsub_v1/subscriber/_consumer.py", line 234, in _blocking_consume
for response in response_generator:
File "/path/to/grpc/_channel.py", line 348, in __next__
return self._next()
File "/path/to/grpc/_channel.py", line 342, in _next
raise self
grpc._channel._Rendezvous: <_Rendezvous of RPC that terminated with (StatusCode.UNAVAILABLE, The service was unable to fulfill your request. Please try again. [code=8a75])>
I saw that this was referenced in another question but here I am asking to how to handle it properly in Python. I have tried to wrap the request in an exception but it seems to run in the background and I am not able to retry in case of that error.
A somewhat hacky approach that is working for me is a custom policy_class. The default one has an on_exception function that ignores DEADLINE_EXCEEDED. You can make a class that inherits the default and also ignores UNAVAILABLE. Mine looks like this:
from google.cloud import pubsub
from google.cloud.pubsub_v1.subscriber.policy import thread
import grpc
class AvailablePolicy(thread.Policy):
def on_exception(self, exception):
"""The parent ignores DEADLINE_EXCEEDED. Let's also ignore UNAVAILABLE.
I'm not sure what triggers that error, but if you ignore it, your
subscriber seems to work just fine. It's probably an intermittent
thing and it reconnects later if you just give it a chance.
"""
# If this is UNAVAILABLE, then we want to retry.
# That entails just returning None.
unavailable = grpc.StatusCode.UNAVAILABLE
if getattr(exception, 'code', lambda: None)() == unavailable:
return
# For anything else, fallback on super.
super(AvailablePolicy, self).on_exception(exception)
subscriber = pubsub.SubscriberClient(policy_class=AvailablePolicy)
# Continue to set up as normal.
It looks a lot like the original on_exception just ignores a different error. If you want, you can add some logging whenever the exception is thrown and verify that everything still works. Future messages will still come through.

Categories