🎄teratailクリスマスプレゼントキャンペーン2024🎄』開催中!

\teratail特別グッズやAmazonギフトカード最大2,000円分が当たる!/

詳細はこちら
Raspbian

Raspbianは、DebianベースのRaspberry Pi用ディストリビューション。ハードウェア浮動小数点演算を有効にすることが可能で、Webブラウズなどの速度を向上できます。

Python 3.x

Python 3はPythonプログラミング言語の最新バージョンであり、2008年12月3日にリリースされました。

Raspberry Pi

Raspberry Piは、ラズベリーパイ財団が開発した、名刺サイズのLinuxコンピュータです。 学校で基本的なコンピュータ科学の教育を促進することを意図しています。

Python

Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

Q&A

解決済

1回答

4261閲覧

python3 asyncの動作が分からない

skyOC

総合スコア1

Raspbian

Raspbianは、DebianベースのRaspberry Pi用ディストリビューション。ハードウェア浮動小数点演算を有効にすることが可能で、Webブラウズなどの速度を向上できます。

Python 3.x

Python 3はPythonプログラミング言語の最新バージョンであり、2008年12月3日にリリースされました。

Raspberry Pi

Raspberry Piは、ラズベリーパイ財団が開発した、名刺サイズのLinuxコンピュータです。 学校で基本的なコンピュータ科学の教育を促進することを意図しています。

Python

Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

0グッド

1クリップ

投稿2020/05/09 13:19

前提・実現したいこと

ラズベリーパイ3上でdiscord.pyを動かし、discordでコメントにGPIO出力で反応したり、GPIO入力があればbotがコメントする物が作りたいです。

発生している問題・エラーメッセージ

discord.py 1.4.0a ドキュメント(https://discordpy.readthedocs.io/ja/latest/quickstart.html)のサンプルコードを元に改変してbotを動かしてコメントに反応したり、コメントに応じてGPIOから出力することは出来ましたが、async関数の動きがよく分からずGPIO入力に応じてbotにコメントさせる方法が分かりませんでした。

該当のソースコード

Python3

1import discord 2import time 3import RPi.GPIO as GPIO 4import sys 5 6GPIO.setmode(GPIO.BOARD) 7GPIO.setup(3, GPIO.IN, pull_up_down=GPIO.PUD_UP)#ボタン1 8GPIO.setup(7, GPIO.OUT)#サーボ 9GPIO.setup(11, GPIO.IN, pull_up_down=GPIO.PUD_UP)#ボタン2 10GPIO.setup(12,GPIO.OUT)#インジケータLED 11 12p = GPIO.PWM(7, 50) 13p.start(0.0) 14 15client = discord.Client() 16 17@client.event 18async def on_ready(): 19 print('We have logged in as {0.user}'.format(client)) 20 21@client.event 22async def on_message(message): 23 if message.author == client.user: 24 return 25 26 if message.content.startswith('サーボ上げる'): 27 await message.channel.send('下げました!') 28 p.ChangeDutyCycle(10) 29 time.sleep(1.0) 30 p.ChangeDutyCycle(0.0) 31 return 32 33 if message.content.startswith('サーボ下げる'): 34 await message.channel.send('下げました!') 35 p.ChangeDutyCycle(5.1) 36 time.sleep(1.0) 37 p.ChangeDutyCycle(0.0) 38 39 40client.run('BotのID') 41

試したこと

async def on_message(message)はメッセージが来たときのみ作動するようだったので、見よう見まねで新しくasyncを作ってみましたが動作しませんでした。
新しくループを作り、その中にon_messageとon_readyとGPIO入力を入れてやれば動作するのかなとは思っていますが、動作がよく分からず難航しています。

補足情報(FW/ツールのバージョンなど)

Raspberry Pi3 modelB(Raspbian Buster)
python 3.7.3

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答1

0

ベストアンサー

※ 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がフリーズするといったように、
    非同期処理内でも同様、時間のかかる処理をすると、その間他の非同期処理が実行されないという事が起こります。


python

1""" 2このプログラムは、非同期で処理されるdiscord と 3別スレッドで動作するGPIOのコールバックとの間でのメッセージ受け渡しを再現するデモです。 4 5非同期IOとスレッドの連携のデモとして書いたコードなので、 6実際のdiscord/GPIOの利用には手直しが必要になる点をご了承ください。 7 8discord, GPI は用いず、動作を模倣した仮のクラスを作って行います。 9このコードの実行には discord.py と RPi.GPIO のインストールは不要です。 10 11 12事前にインストールが必要な外部ライブラリ: "janus" 13 14非同期/同期での読書が可能なQueueの実装です。 15 16- janus(https://pypi.org/project/janus/) ライブラリ 17 pip install janus でインストール 18""" 19 20import time 21import asyncio 22from threading import Thread 23 24from janus import Queue 25 26# ---------- 最初の MockGPIO, MockDiscord は意図的に読み飛ばしてください -------- 27 28class MockGPIO(Thread): 29 """ 30 NOTE: GPIO.start() 時に run() が呼び出される。 31 """ 32 33 def __init__(self, *args, **kw): 34 super().__init__(*args, **kw) 35 self._callback = None 36 37 def run(self): 38 # 別スレッドで稼働。ここではブロッキング操作(time.sleep)は可能 39 for num in range(10): # <-- 仮の入力データ [0..9] 40 if self._callback: 41 self._callback(num) 42 time.sleep(1) 43 44 def add_event_detect(self, callback=None): 45 if callback: 46 self.add_event_callback(callback) 47 # ここは実際にも、add_event_detectを呼び出す事で、新スレッドが立ち上がります。 48 self.start() # Thread.start -> 別スレッドでrun()を実行 49 50 def add_event_callback(self, callback): 51 self._callback = callback 52 53 54class MockDiscordClient: 55 56 def __init__(self): 57 self._callback = None 58 59 async def run_event(self, *args, **kw): 60 await self._callback(*args, **kw) 61 62 async def start(self, *args): 63 # 非同期で動作。 64 # 通常のtime.sleep 等、プログラムの実行を阻害する(ブロッキング)操作は、 65 # asyncioのループを止めてしまうのでNG。非同期での代替の手段を取ります。 66 for num in range(10): # <-- 仮の入力データ [0..9] 67 await asyncio.sleep(1) 68 await self.run_event(num) 69 70 def event(self, coro): 71 if asyncio.iscoroutinefunction(coro): 72 self._callback = coro 73 return coro 74 75 async def send(self, msg): 76 print("send to discord >>>", msg) 77 await asyncio.sleep(1) 78 79 80GPIO = MockGPIO(daemon=True) 81client = MockDiscordClient() 82 83## 上は GPIO モジュールと discord をテスト用にエミュレートする為のクラスです 84## ユーザ視点からは中の実装をそれ程詳細に把握する必要はありません。 85 86## 実際の実装に合わせて模倣している要点は2つ 87## * discoard は非同期IO(asyncio)で扱われる 88## * GPIO のコールバックは別スレッドで動いている 89 90# ------------------------ 解説ここから ------------------------ 91 92async def main(): 93 94 ## 95 # (1) キューを用いてメッセージのやり取りの(準備) 96 # 97 # janus.Queue (sync <=> async queue) 98 queue = Queue() 99 sync_q = queue.sync_q # GPIO側で使う同期キュー 100 async_q = queue.async_q # awaitで使う非同期キュー 101 102 ## 103 # (2) GPIO からの入力に応じてコールバックを呼び出し 104 # 105 # ※ GPIO については簡略化してるので、実際の呼び出し方と異なります。 106 # 実際の呼び出し方は add_event_detect, add_event_callback 辺りを調べて見て下さい。 107 # 108 # ※ 更に、実際の利用は、discord のon_ready等で discord の準備が出来てから登録する。 109 def callback(num): 110 # GPIO -> callback -> 同期キューにput 111 print("GPIO >>>", num) 112 sync_q.put(num) 113 114 GPIO.add_event_detect(callback=callback) 115 116 ### 117 # (3) ディスコードのメッセージ受信 118 # 119 # ※ 実際のdiscordライブラリにこのような名前のイベントはありません。 120 # discordのclient と 他の非同期ループを扱う為のデモとして仮実装。 121 @client.event 122 async def on_discord_message(msg): 123 await asyncio.sleep(1) 124 print("recv from discord <<<", msg) 125 126 ## 127 # (4) キュー内のメッセージを非同期で読み出し 128 # 129 # GPIOのコールバックで sync.put で送られたものを async_q.get で読み出します 130 # 131 async def listen_queue_loop(): 132 while True: 133 msg = await async_q.get() 134 # ディスコードへ送信 (※ 実際はチャンネルに送信) 135 await client.send(str(msg)) 136 137 ## 138 # (5) 非同期ループの実行 (client.runの置き換え) 139 # - asyncio.gather 2つの非同期ループの実行に用います 140 # - client.start 141 # - listen_queue_loop 142 # 143 # 非同期処理: ディスコードclient と、キューの読み出しの開始 144 145 BOT_TOKEN = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" 146 await asyncio.gather(client.start(BOT_TOKEN), listen_queue_loop()) 147 148 149if __name__ == '__main__': 150 try: 151 asyncio.run(main()) 152 except KeyboardInterrupt: 153 pass 154

投稿2020/05/12 00:45

編集2020/05/12 06:09
teamikl

総合スコア8744

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

skyOC

2020/05/12 11:47

とても丁寧な回答ありがとうございます。 もうここまで解説して頂ければもうゴールは見えているので週末試してみようと思います。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.36%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問