Exploring IoT Integration: Experiments embedded programing with the ChatGPT (GPT API). Control Raspberry Pi GPIO by GPT API.
前回に続き ChatGPT ネタです。
ChatGPT や、そのプラグインを触っていると、組込系技術者の方であれば ChatGPT で IoT 的な組込制御ができないかなあ、と考えるのではないでしょうか。
例えば、ChatGPT に
部屋の照明を点けて
とか、
エアコンの設定を 20度にして
とか、はたまた
○○さんに、会議中止の件のメールを送って(← これは組込の例じゃないな)
とか話しかけることで、以前に流行ったホームアシスタントのようなことができたら面白そうです。
実は、ChatGPT を実装している OpenAI API にはそのような機能があって、自分が書いたプログラムコード(関数)を ChatGPT から呼び出して貰うことが可能なのだそうです。すごい!! (なお、ここからは ChatGPT ではなく、GPT と呼ぶことにします。ChatGPT という用語は、ウェブインターフェイスによる GPT アプリのことを指すときだけに使うことにします。)
実際には GPT が直接関数を呼び出すのではなくて、ユーザーが関数の一覧を GPT に教えることで、文脈にあった関数と、その関数に与えるべき引数を GPT が選んでくれる、という仕組なのですが。
この技術に関する OpenAI のブログは、こちらにあります。
また、公式チュートリアルはこちらにあります。
最初これらの説明を読んだときはピンと来なかったのですが、よくよく考えてみると、ウェブアプリケーションだけでなく、IoT 的な用途にも使えそうな気がしてきました。
GPT の API は Python でなくても利用できますし、例えばマイクの付いた ESP32 マイコンなどを使い、さらに OpenAI Whisper と組み合わせることで、音声入力による IoT 制御をすることもできそうですが、まずは音声ではなく、テキスト形式で試してみることにしました。
ちなみに、ChatGPT のプラグインが続々と公開されている割には、GPT API の “Function Call” に関するチュートリアルはまだ充実しているとは言えない状況で、先例を探すのにはちょっと苦労しました。私も試し始めたばかりなのですが、以下の稚拙な試行が皆様の御参考になればと思い、公開させて頂きます。
まずは動作例から
さて。プログラムコードの全体は最後に示しますが、まずは、動作例から示します。なお、このプログラムは Raspberry Pi 上で動かせば実際に GPIO 制御ができると思いますが、PC 上でも試せるように、デモ機能を設けてあります。
太字の Query: より後ろが GPT に対する指示で、その後ろの行が GPIO 制御関数の呼び出し(のデモ)に相当します。
Query: 3+5はいくつですか?
Answer: 3 + 5 は 8 です。
Query: GPIOの3番をオフにしてください。
setup_gpio(3, GPIO.OUT) GPIO.output(3, False) Answer: GPIOの3番がオフに設定されました。
Query: GPIOの6番の値を教えてください。
setup_gpio(6, GPIO.IN) value = GPIO.input(6) value: False Answer: GPIOの6番の値は、Falseです。
Query: GPIOの2番の値を読み出して、その値をGPIOの5番に出力してください。
setup_gpio(2, GPIO.IN) value = GPIO.input(2) value: False setup_gpio(5, GPIO.OUT) GPIO.output(5, False) Answer: GPIOの2番の値はFalseです。その値をGPIOの5番に出力しました。
Query: GPIOの0番から2番を全てオンにしてください。
setup_gpio(0, GPIO.OUT) GPIO.output(0, True) setup_gpio(1, GPIO.OUT) GPIO.output(1, True) setup_gpio(2, GPIO.OUT) GPIO.output(2, True) Answer: GPIOの0番から2番を全てオンにしました。
Query: GPIOの0番から3番を、偶数ピンは全部オフに、奇数ピンは全部オンにして。
setup_gpio(0, GPIO.OUT) GPIO.output(0, False) setup_gpio(1, GPIO.OUT) GPIO.output(1, True) setup_gpio(2, GPIO.OUT) GPIO.output(2, False) setup_gpio(3, GPIO.OUT) GPIO.output(3, True) Answer: GPIOの0番から3番のピンに対して、偶数番号のピンはすべてオフに設定し、奇数番号のピンはすべてオンに設定しました。
実は、OpenAI の公式チュートリアルを読めば、最初の query(3+5。これは指定した関数を呼び出さない)と、続く 2つの query は比較的簡単に書けます。問題は 3番目以降です。ポイントは openai.ChatCompletion.create()
を呼び出すときには毎回 functions
を与えることと、そのレスポンスに function_call
が含まれるときは、繰り返し openai.ChatCompletion.create()
を呼び出してあげることのようです。
GPIO の制御コード
まず最初に、GPT に呼び出して貰う(正確には関数名と与える引数を生成して貰う)関数を用意します。ちなみにこのコードはテストしていないのですが、コード自体も ChatGPT に書いて貰いました。(JSON 形式の応答を返す部分を除く。)
import RPi.GPIO as GPIO
def setup_gpio(pin, mode):
GPIO.setmode(GPIO.BCM)
GPIO.setup(pin, mode)
def set_gpio(pin, value):
setup_gpio(pin, GPIO.OUT)
GPIO.output(pin, value)
return json.dumps({"error": False})
def get_gpio(pin):
setup_gpio(pin, GPIO.IN)
value = GPIO.input(pin)
return json.dumps({"pin": pin, "read_value": value})
関数の一覧
関数群(ここでは上記の set_gpio()
と get_gpio()
)について、関数の役割と引数の役割を記述します。GPT はこれを解釈して、与えられた文脈からどの関数を、どのような引数で呼び出したら良いか考えてくれます。
FUNCTIONS = [
{
"name": "set_gpio",
"description": "Set the specified value to the specified GPIO pin.",
"parameters": {
"type": "object",
"properties": {
"pin": {
"type": "integer",
"description": "GPIO pin number",
},
"value": {
"type": "boolean",
"description": "value to the GPIO pin",
},
},
"required": ["pin", "value"],
},
},
{
"name": "get_gpio",
"description": "Read the value of the specified GPIO pin "
+ "and return it as either True or False.",
"parameters": {
"type": "object",
"properties": {
"pin": {
"type": "integer",
"description": "GPIO pin number",
},
},
"required": ["pin"],
},
},
]
プログラムコード全体
最後にコード全体を示します。上記と重複がありますが、簡単に再現できるよう、全体を再掲します。なお、.env というファイルの中 OPENAI_API_KEY
を定義しており、dotenv ライブラリで取り込んでいます。
DEBUG = False
import json
import os
import openai
from dotenv import load_dotenv
try:
import RPi.GPIO as GPIO
demo = False
except:
import random
demo = True
load_dotenv()
openai.api_key = os.getenv("OPENAI_API_KEY")
# GPIO Control Functions
def setup_gpio(pin, mode):
GPIO.setmode(GPIO.BCM)
GPIO.setup(pin, mode)
def set_gpio(pin, value):
if demo:
print("setup_gpio({}, GPIO.OUT)".format(pin))
print("GPIO.output({}, {})".format(pin, value))
else:
setup_gpio(pin, GPIO.OUT)
GPIO.output(pin, value)
return json.dumps({"error": False})
def get_gpio(pin):
if demo:
print("setup_gpio({}, GPIO.IN)".format(pin))
print("value = GPIO.input({})".format(pin))
value = random.randint(0, 1) == 1
else:
setup_gpio(pin, GPIO.IN)
value = GPIO.input(pin)
print("value: {}".format(value))
return json.dumps({"pin": pin, "read_value": value})
FUNCTIONS = [
{
"name": "set_gpio",
"description": "Set the specified value to the specified GPIO pin.",
"parameters": {
"type": "object",
"properties": {
"pin": {
"type": "integer",
"description": "GPIO pin number",
},
"value": {
"type": "boolean",
"description": "value to the GPIO pin",
},
},
"required": ["pin", "value"],
},
},
{
"name": "get_gpio",
"description": "Read the value of the specified GPIO pin "
+ "and return it as either True or False.",
"parameters": {
"type": "object",
"properties": {
"pin": {
"type": "integer",
"description": "GPIO pin number",
},
},
"required": ["pin"],
},
},
]
def send_query(query):
messages = [{"role": "user", "content": query}]
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo-0613",
messages=messages,
functions=FUNCTIONS,
)
if DEBUG:
print(response)
return messages, response
def call_function(messages, response_message):
func_name = response_message["function_call"]["name"]
func_args = json.loads(response_message["function_call"]["arguments"])
if func_name == "set_gpio":
func_response = set_gpio(func_args.get("pin"), func_args.get("value"))
elif func_name == "get_gpio":
func_response = get_gpio(func_args.get("pin"))
if func_response:
messages.append(response_message)
messages.append(
{
"role": "function",
"name": func_name,
"content": func_response,
}
)
# Get a new response from GPT where it can see the function response
second_response = openai.ChatCompletion.create(
model="gpt-3.5-turbo-0613",
messages=messages,
functions=FUNCTIONS,
)
return second_response
else:
return None
def chat(query):
print()
print("Query:", query)
messages, response = send_query(query)
msg = response["choices"][0]["message"]
while msg.get("function_call"):
response = call_function(messages, msg)
if response:
# print("if response:", response.choices[0]["message"]["content"])
msg = response["choices"][0]["message"]
else:
print("# No function response")
return
if DEBUG:
print("# Function call not required")
if msg.get("content"):
print("Answer:", msg.content)
return
chat("3+5はいくつですか?")
chat("GPIOの3番をオフにしてください。")
chat("GPIOの6番の値を教えてください。")
chat("GPIOの2番の値を読み出して、その値をGPIOの5番に出力してください。")
chat("GPIOの0番から2番を全てオンにしてください。")
chat("GPIOの0番から3番を、偶数ピンは全部オフに、奇数ピンは全部オンにして。")
私もまだまだ触り始めたばかりなので、もっと綺麗な書き方などあると思いますが、今回は御容赦ください。今後も、もう少しコードの改善を図っていきたいと思います。
今日はここまで。