ChatGPT にラズパイ GPIO を制御して貰おう!(全ソースコード付)

(更新

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番を、偶数ピンは全部オフに、奇数ピンは全部オンにして。")

私もまだまだ触り始めたばかりなので、もっと綺麗な書き方などあると思いますが、今回は御容赦ください。今後も、もう少しコードの改善を図っていきたいと思います。

今日はここまで。

お問い合わせはお気軽に!

お問い合わせを頂いた後、継続して営業活動をしたり、ニュースレター等をお送りしたりすることはございません。
御返答は 24時間以内(営業時間中)とさせて頂いております。必ず返信致しますが、時々アドレス誤りと思われる返信エラーがございます。返答が届かない場合、大変お手数ではございますが別のメールアドレスで督促頂けますと幸いです。