質問: client.user: return if message.content.startswith('サーボ上げる'): await message.channel.send('下げました!') p.ChangeDutyCycle(10) time.sleep(1.0) p.ChangeDutyCycle(0.0) return if message.content.startswith('サーボ下げる'): await message.channel.send('下げました!') p.ChangeDutyCycle(5.1) time.sleep(1.0) p.ChangeDutyCycle(0.0) client.run('BotのID') ``` ### 試したこと async def on_message(message)はメッセージが来たときのみ作動するようだったので、見よう見まねで新しくasyncを作ってみましたが動作しませんでした。 新しくループを作り、その中にon_messageとon_readyとGPIO入力を入れてやれば動作するのかなとは思っていますが、動作がよく分からず難航しています。 ### 補足情報(FW/ツールのバージョンなど) Raspberry Pi3 modelB(Raspbian Buster) python 3.7.3
回答: ※ GPIO は使ったことがないので、一般的な「非同期」と「同期」の間の連携方法として回答します。 まず前提として discord.py は非同期IOで扱われている為、 asyncioのイベントループ外から直接メッセージを送る事は出来ません。 解決案として提示できるのは以下の2通り。 * A. 「別スレッド」から「非同期ループで処理される関数」にメッセージを送る方法 非同期<=>同期 をサポートした Queue の実装。 "janus" ライブラリを使う * B. GPIO の入出力を非同期IOで扱うライブラリ "apigpio" 末尾のコードで主にAの方法について解説します A. 「別スレッド」から「非同期ループで処理される関数」にメッセージを送る方法 ``` 概要: - 非同期<=>同期間連携の為のQueue を準備 - janus のインストール (pip install janus) - 同期処理 - GPIO: callback で通知を受け取る (別スレッドで実行される) - Queue へメッセージ送信(put) - 非同期処理 - Queue からの値を読み出す(get)処理のループ - 通常の discord の処理のループ - asyncio.gather で複数の非同期ループを纏める - asyncio.run 非同期処理のイベントループを開始 ``` B. GPIO を非同期IOで扱うライブラリ "apigpio" "pigpio" というライブラリがありそれを非同期に扱う為のモノの様です。 apigpio のサンプルコードを読む上での注意点: サンプルコードの対応しているPythonのバージョンが古いスタイルの記述になっています。 実行には支障ないと思いますが以下のように読み替えます。 * `@asyncio.coroutine` -> `async` * `yield from` -> `await` 実際に試してないので軽く紹介のみ。 `yield` が何かわからない場合、 前提知識として「ジェネレータ」辺り迄は理解しておいた方が良いと思います。 全体像はこのような感じになります(概要のみ) * apigpio GPIOを扱う非同期IO * discord を扱う非同期IO (client.runの代わりにclient.startを利用) * NOTE: 非同期同士の連携には asyncio.Queue が使えます。 * asyncio.gatherで2つの非同期処理をまとめる (後述のコードを参考にして下さい) * asyncio.runで開始 async/await について 大雑把な説明です。実際のコーディングの際の注意点に焦点を当てました。 * async は、awaitを用いる関数 の定義等で使います。async付きで定義された関数を呼び出すと「コルーチン」を返します。 asyncを付けると関数自体が自動的に非同期になったりは **しません** (よくある誤解1) 関数内のコードの実行自体は通常と同じように処理されます。 その為、非同期処理の中ではコードの実行を止めるような操作はNG (エラーにはなりませんが、他の非同期処理が出来なくなり期待する動作になりません) 非同期(async)の関数(コルーチン)内で出来ない事: * time.sleep等のコードの実行を止める操作。同一スレッド内ではNG * await を伴わない無限ループ。await がないとその間他の非同期処理が実行されません。 * ブロッキング I/O 操作 (ファイル書込やsocketからの読書等) * await では、値が返されるまで処理を待ちます。 但し、await を付けると自動的にその部分で待つようになったりは **しません** (よくある誤解2) awaitの右側は、await可能な(awaitable)オブジェクトである必要があります。 具体的には「コルーチン」「フューチャー」「タスク」等。説明は省きますが 重要な点は、 await で使えるという共通の性質(awaitable)を持ちます。 * asyncio.run (client.run内部) 非同期処理のイベントループの正体ですが、単一スレッド上で動作するループです。 各非同期処理(コルーチン)自体は、通常の関数と同様に実行されます。 順番に実行される * 非同期処理A await で イベントループに戻る * 非同期処理B await で イベントループに戻る * 非同期処理A 以前の await から再開、次のawait でまた イベントループに戻る 繰り返し説明になりますが、今迄「非同期処理」でやってはいけない事として説明した理由がここで、 各非同期処理を実行する為に、イベントループは動き続ける必要があります。 つまり、非同期処理内では、コードの実行を中断するようなブロッキング操作をしてはいけない。 これは、GUIプログラミングに於いてのイベントループと同じで、 イベント処理内で時間のかかることをすると、GUIがフリーズするといったように、 非同期処理内でも同様、時間のかかる処理をすると、その間他の非同期処理が実行されないという事が起こります。 ``` """ このプログラムは、非同期で処理されるdiscord と 別スレッドで動作するGPIOのコールバックとの間でのメッセージ受け渡しを再現するデモです。 非同期IOとスレッドの連携のデモとして書いたコードなので、 実際のdiscord/GPIOの利用には手直しが必要になる点をご了承ください。 discord, GPI は用いず、動作を模倣した仮のクラスを作って行います。 このコードの実行には discord.py と RPi.GPIO のインストールは不要です。 事前にインストールが必要な外部ライブラリ: "janus" 非同期/同期での読書が可能なQueueの実装です。 - janus(https://pypi.org/project/janus/) ライブラリ pip install janus でインストール """ import time import asyncio from threading import Thread from janus import Queue # ---------- 最初の MockGPIO, MockDiscord は意図的に読み飛ばしてください -------- class MockGPIO(Thread): """ NOTE: GPIO.start() 時に run() が呼び出される。 """ def __init__(self, *args, **kw): super().__init__(*args, **kw) self._callback = None def run(self): # 別スレッドで稼働。ここではブロッキング操作(time.sleep)は可能 for num in range(10): # <-- 仮の入力データ [0..9] if self._callback: self._callback(num) time.sleep(1) def add_event_detect(self, callback=None): if callback: self.add_event_callback(callback) # ここは実際にも、add_event_detectを呼び出す事で、新スレッドが立ち上がります。 self.start() # Thread.start -> 別スレッドでrun()を実行 def add_event_callback(self, callback): self._callback = callback class MockDiscordClient: def __init__(self): self._callback = None async def run_event(self, *args, **kw): await self._callback(*args, **kw) async def start(self, *args): # 非同期で動作。 # 通常のtime.sleep 等、プログラムの実行を阻害する(ブロッキング)操作は、 # asyncioのループを止めてしまうのでNG。非同期での代替の手段を取ります。 for num in range(10): # <-- 仮の入力データ [0..9] await asyncio.sleep(1) await self.run_event(num) def event(self, coro): if asyncio.iscoroutinefunction(coro): self._callback = coro return coro async def send(self, msg): print("send to discord >>>", msg) await asyncio.sleep(1) GPIO = MockGPIO(daemon=True) client = MockDiscordClient() ## 上は GPIO モジュールと discord をテスト用にエミュレートする為のクラスです ## ユーザ視点からは中の実装をそれ程詳細に把握する必要はありません。 ## 実際の実装に合わせて模倣している要点は2つ ## * discoard は非同期IO(asyncio)で扱われる ## * GPIO のコールバックは別スレッドで動いている # ------------------------ 解説ここから ------------------------ async def main(): ## # (1) キューを用いてメッセージのやり取りの(準備) # # janus.Queue (sync <=> async queue) queue = Queue() sync_q = queue.sync_q # GPIO側で使う同期キュー async_q = queue.async_q # awaitで使う非同期キュー ## # (2) GPIO からの入力に応じてコールバックを呼び出し # # ※ GPIO については簡略化してるので、実際の呼び出し方と異なります。 # 実際の呼び出し方は add_event_detect, add_event_callback 辺りを調べて見て下さい。 # # ※ 更に、実際の利用は、discord のon_ready等で discord の準備が出来てから登録する。 def callback(num): # GPIO -> callback -> 同期キューにput print("GPIO >>>", num) sync_q.put(num) GPIO.add_event_detect(callback=callback) ### # (3) ディスコードのメッセージ受信 # # ※ 実際のdiscordライブラリにこのような名前のイベントはありません。 # discordのclient と 他の非同期ループを扱う為のデモとして仮実装。 @client.event async def on_discord_message(msg): await asyncio.sleep(1) print("recv from discord <<<", msg) ## # (4) キュー内のメッセージを非同期で読み出し # # GPIOのコールバックで sync.put で送られたものを async_q.get で読み出します # async def listen_queue_loop(): while True: msg = await async_q.get() # ディスコードへ送信 (※ 実際はチャンネルに送信) await client.send(str(msg)) ## # (5) 非同期ループの実行 (client.runの置き換え) # - asyncio.gather 2つの非同期ループの実行に用います # - client.start # - listen_queue_loop # # 非同期処理: ディスコードclient と、キューの読み出しの開始 BOT_TOKEN = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" await asyncio.gather(client.start(BOT_TOKEN), listen_queue_loop()) if __name__ == '__main__': try: asyncio.run(main()) except KeyboardInterrupt: pass ```