ChatGPTとローカルLLMでロボットアームを動かして見た!

このサイトはアフィリエイト広告を含みます
AI

生成AIでロボットアームを動かす方法

最近、ChatGPT や生成AIが話題ですが、そこからさらに生成AIを利用して、ロボットを動かすフィジカルAIというのが注目されています。
今回は、フィジカルAIと言って良いのかわかりませんが、音声でAIに命令を理解させ、実際にロボットアームを動かす方法を紹介します。

完成品は動画のようなことができます。

音声認識やArduino制御、ローカルLLMも組み合わせることで、AIと会話しながらロボットを操作できる環境を構築しています。

ロボットアームの作り方について

ロボットアームの作り方は下の記事で解説しています。

是非読んでみてください。

コスパ最強!? ロボットアームの作り方_その①
自作ロボットアームの紹介ロボットアームを自作したい方向けに、Arduinoを使った6軸ロボットアームの作り方をまとめました。本記事では、初心者でも再現できる構成で・必要な部品・組み立て手順・プログラム制御・よくあるトラブル(サーボが震える/…

使用するAIについて

今回使用するAIは、人間とテキストベースで会話する言語モデル:LLM(large language model)、音声をテキストに変換するSTTモデル(speech to text)、テキストを音声に変換するTTSモデル(text to speech)の3つを利用しています。

使用モデル

①言語モデル(LLM):ChatGPT APIまたはローカルLLM(gemma 2)

②音声➡︎テキストモデル(STT):faster-whisper

③テキスト➡︎音声モデル(TTS):VOICEVOX

注意点

はじめに、お断りしておかないといけないのですが、上の動画を撮影した2023年時点では、STTモデル、TTSモデルともにCPU版を利用していたため、応答にかなり時間がかかっています。

そのため、動画内では、返答までの時間を編集でカットしています。

今回の2026年版では、応答速度を上げるため、STTモデル、TTSモデルともにGPU版を利用しました。

その際の実際の応答速度は下の動画となります。

会話用のAI(LLM)について

会話用のAI(LLM)は、ChatGPTのAPIを利用する方法と自分のパソコンでローカルLLMを利用する場合の二つのパターンを用意しました。

それぞれのメリットとデメリットは以下の通りです。

ChatGPT API

ChatGPTなので、かなり高性能です。

適当な指示でも汲み取ってくれる可能性が高いです。

ただし、APIの契約が必要で、使用量に応じて、利用料金が発生します。

ローカルLLM

こちらは、自宅のPCで生成AIを実行するタイプです。

ChatGPTに比べると流石に性能は落ちますが、今回の用途であれば、性能的には十分です。

上記動画はこちらのローカルLLMで実行しています。

メリットは自宅PCで実行できるので、無料で動かせることです。

ただし、実行にはGPUが必要ですので、ご注意ください。

今回はGoogleから公開されているオープンソースのAIモデル:gemma 2-9B-itを利用していますが、GPUのスペック次第でより高性能なモデルも利用できます。

ローカルLLMの具体的な設定方法は、下の記事で説明していますので、是非見てください。

初心者向け|自宅PCでローカルLLMを動かす方法【CUDA・GPU選びも解説】
自宅PCで生成AI(ローカルLLM)の動かし方ChatGPTのような生成AIを、自宅PCで動かせる「ローカルLLM」が注目されています。高性能GPUを搭載したPCがあれば、オフライン環境でもAIチャットや音声認識、画像解析などを楽しめます。…

VOICEVOX

音声生成には、VOICEVOXを使用(VOICEVOX:ずんだもん)しています。

キャラクターごとに利用規約があるため、使用時はご注意ください。

ソフトウェア自体は無料で以下サイトからダウンロードできます。

ありがたいことに、ずんだもんはクレジット表記をすれば、無料で使用可能です。

Releases · VOICEVOX/voicevox_engine
無料で使える中品質なテキスト読み上げソフトウェア、VOICEVOXの音声合成エンジン. Contribute to VOICEVOX/voicevox_engine development by creating an account on…

私はWindowsのGPUを使用しており、CUDAも使用しているため、

「Windows(GPU/CUDA版) part 1 part 2」をダウンロードしました。

Part 1、Part 2をそれぞれダウンロードし、同じフォルダでPart 1を展開するとまとめて展開されます。

展開されたフォルダの中にrun.exeがあるので、ダブルクリックすると、VOICEVOXの APIが起動します。

コードについて

プログラムの構成

全体の処理の流れとしては、以下のようになっています。

  1. マイクから音声を入力
  2. faster-whisperで音声をテキスト化(STT)
  3. LLM(ChatGPT API または ローカルLLM)が命令内容を解析
  4. VOICEVOXでAIの返答音声を生成・再生(TTS)
  5. Arduinoへシリアル通信でコマンドを送信
  6. ロボットアームが実際に動作

こちらがプログラムの全体構成です。

├─ app.py
│  └─ Pythonの本体コード全体の制御
│     ・音声認識LLM処理音声合成の各モジュールを呼び出し連携
│     ・生成された音声ファイルの再生
│     ・Arduinoへコマンド数値などを送信

├─ stt_engine.py
│  └─ 音声認識用
│     ・マイク録音の制御
│     ・faster-whisper による音声認識テキスト化

├─ tts_engine.py
│  └─ テキストtoスピーチ用
│     ・音声合成モデルを扱う処理テキストから音声ファイルを生成

├─ llm_backend.py
│  └─ LLM切替応答処理用
│     ・config.pyの設定に応じて ChatGPT API または ローカルLLM を呼び出し
│     ・LLMの応答テキストから命令コマンドを抽出変換

├─ config.py
│  └─ 設定まとめ
│     ・.envから環境変数を読み込む
│     ・LLM種別api / local)、APIキーローカルLLMのURLやモデル名を提供


├─ .env
│  └─ 秘密情報環境変数
│     ・LLM_MODEAPIかローカルかの切り替え
│     ・OPENAI_API_KEYOPENAI_MODEL
│     ・LOCAL_LLM_URLLOCAL_LLM_MODEL



│  ##### ここから別環境 #####
├─ llm_server.py
│  └─ ローカルLLM実行用PC内サーバー)


└─ Arduino/
   └─ LLM_robot_arm_arduino.ino
      └─ ロボットアームを制御するArduinoコード


ライブラリのインストール

ロボットアーム用のコードとLLM用のコードを用意しており、私は仮想環境を分けています。

その方が、ライブラリが干渉するリスクがないので、安全です。

◼️ロボットアーム用のライブラリ

# 1. 設定管理通信用APIキー等の読み込みやLLMとの通信
pip install python-dotenv openai requests

# 2. 音声認識用マイク録音とfaster-whisperによる文字起こし
pip install faster-whisper sounddevice soundfile numpy

# 3. ロボットアーム制御用Arduinoとのシリアル通信
pip install pyserial

◼️ローカルLLM用のライブラリ

ローカルLLMに必要なライブラリは下の記事で説明していますが、追加で、以下のライブラリも必要です。

#FastAPI本体とサーバー起動用のuvicornをインストールします
pip install fastapi uvicorn
初心者向け|自宅PCでローカルLLMを動かす方法【CUDA・GPU選びも解説】
自宅PCで生成AI(ローカルLLM)の動かし方ChatGPTのような生成AIを、自宅PCで動かせる「ローカルLLM」が注目されています。高性能GPUを搭載したPCがあれば、オフライン環境でもAIチャットや音声認識、画像解析などを楽しめます。…

プログラム実行手順

プログラムの実行は、下の順番で実行します。

1から3はどの順番でも問題なく、4を最後に実行して貰えば、OKです。

  1. ローカルLLM用APIの「llm_server.py
  2. VOICEVOXの「run.exe
  3. ロボットアーム用の「LLM_robot_arm_arduino.ino
  4. 統括用の「app.py

終了時は、音声操作で「EXIT(イグジット)」というと終了します。

コード

最後にコードを紹介します。

◼️ 編集箇所①

config.py」は各種モデル用の設定ファイルです

そのままでも大丈夫ですが、必要に応じて編集してください。

◼️ 編集箇所②

「.env」はChatGPT APIのAPIを記載したり、APIとローカルLLMを切り替えたりすると用の設定ファイルです。

お使いの環境に合わせて編集してご利用ください。

◼️ 編集箇所③

llm_server.pyはローカルLLM用のコードです。

ローカルLLMを利用される場合は、モデルのパスを編集ください。

app.py

# app.py
from llm_backend import ask_llm
from tts_engine import generate_voice
from stt_engine import listen_and_transcribe
import winsound
import time
import serial

# ===== シリアル通信設定 =====
# configからシリアル設定をインポート
from config import SERIAL_PORT, SERIAL_BAUDRATE

EXIT_PHRASE = "EXIT"

# configの値を使用する
ser = serial.Serial(SERIAL_PORT, SERIAL_BAUDRATE)


def robot_arm(val):
    """
    過去コードと同じ方式
    1文字のコマンドを100回送信する
    """
    val = str(val)

    for i in range(100):
        ser.write(bytes(val, "utf-8"))
        time.sleep(0.01)


def main():
    print("常時監視版 音声対話")
    print("終了するには exit または 終了 と話してください")

    # ===== シリアル通信用初期動作 =====
    robot_arm("1")
    time.sleep(1)  # Arduino側との接続待ち

    while True:
        text = listen_and_transcribe()

        if not text:
            continue

        text = text.strip()
        print("あなた:", text)

        # ユーザー発話で終了
        if text.lower() == "exit" or "終了" in text or "ストップ" in text:
            print("終了します")
            break

        # ===== LLM =====
        result = ask_llm(text)

        reply = result.get("reply", "")
        commands = result.get("commands", [])

        print("AI:", reply)
        print("commands:", commands)

        # AI側から終了指示
        if reply == EXIT_PHRASE:
            print("終了します")
            break

        # ===== 音声出力 =====
        if reply:
            wav = generate_voice(reply, "output.wav")
            winsound.PlaySound(str(wav), winsound.SND_FILENAME)
            time.sleep(1.0)

        # ===== コマンド処理 =====
        if commands:
            for cmd in commands:
                print("実行コマンド:", cmd)

                robot_arm(cmd)

                time.sleep(1)

    ser.close()


if __name__ == "__main__":
    main()

stt_engine.py

# stt_engine.py
from faster_whisper import WhisperModel
import sounddevice as sd
import soundfile as sf
import numpy as np

# ===== STT設定 =====
from config import (
    WHISPER_MODEL, 
    WHISPER_DEVICE, 
    WHISPER_COMPUTE_TYPE, 
    WHISPER_SAMPLE_RATE as SAMPLE_RATE
)

model = None

def load_model():
    global model
    if model is None:
        model = WhisperModel(
            WHISPER_MODEL,            # configの値を使用
            device=WHISPER_DEVICE,         # configの値を使用
            compute_type=WHISPER_COMPUTE_TYPE # configの値を使用
        )
    return model


# -------- 固定録音 --------
def record_and_transcribe():
    seconds = 5

    audio = sd.rec(
        int(seconds * SAMPLE_RATE),
        samplerate=SAMPLE_RATE,
        channels=1,
        dtype="float32"
    )
    sd.wait()

    sf.write("input.wav", audio, SAMPLE_RATE)
    return transcribe("input.wav")


# -------- 常時監視連続録音版 --------
def listen_and_transcribe():
    block = 0.2
    threshold = 0.01
    silence_limit = 25

    block_size = int(block * SAMPLE_RATE)

    frames = []
    silence = 0
    recording = False

    print("音声待機中...")

    with sd.InputStream(
        samplerate=SAMPLE_RATE,
        channels=1,
        dtype="float32",
        blocksize=block_size
    ) as stream:

        while True:
            data, overflowed = stream.read(block_size)

            if overflowed:
                print("警告: 音声入力が一部欠落しました")

            vol = float(np.sqrt((data ** 2).mean()))

            # 発話開始待ち
            if not recording:
                if vol > threshold:
                    print("録音開始")
                    recording = True
                    frames.append(data.copy())
                continue

            # 録音中
            frames.append(data.copy())

            if vol < threshold:
                silence += 1
            else:
                silence = 0

            # 無音が続いたら録音終了
            if silence > silence_limit:
                print("録音終了")
                break

    if not frames:
        return ""

    audio = np.concatenate(frames)

    sf.write("input.wav", audio, SAMPLE_RATE)
    return transcribe("input.wav")


# -------- 文字起こし --------
def transcribe(path):
    m = load_model()

    segments, _ = m.transcribe(
        path,
        language="ja",
        beam_size=5,
        best_of=5,
        temperature=0.0,
        vad_filter=True,
        condition_on_previous_text=False,
    )

    text = "".join([s.text for s in segments]).strip()
    return text

tts_engine.py

# tts_engine.py
from pathlib import Path
import requests

# config.py から設定値をインポートする
from config import VOICEVOX_URL, VOICEVOX_SPEAKER_ID as SPEAKER_ID

def generate_voice(text, output_path="output.wav"):
    """
    VOICEVOX APIを使ってtextを音声化し、wavファイルに保存する
    app.py側の呼び出し仕様に合わせて Path を返す
    """
    if not text or not text.strip():
        return None

    output_path = Path(output_path)

    # ① 音声クエリ作成
    query_response = requests.post(
        f"{VOICEVOX_URL}/audio_query",
        params={
            "text": text,
            "speaker": SPEAKER_ID,
        },
        timeout=30,
    )
    query_response.raise_for_status()

    # ② 音声合成
    synthesis_response = requests.post(
        f"{VOICEVOX_URL}/synthesis",
        params={
            "speaker": SPEAKER_ID,
        },
        json=query_response.json(),
        timeout=60,
    )
    synthesis_response.raise_for_status()

    # ③ wav保存
    output_path.write_bytes(synthesis_response.content)

    return output_path

llm_backend.py

# llm_backend.py
import requests
import re
from config import *

SYSTEM_PROMPT = """
あなたはロボットアーム操作にも対応できる音声アシスタントです
通常の会話には、1行または2行で自然に簡潔に返答してください

ユーザーがロボットアームの操作を明確に指示した場合は必ず対応する数字を半角ダブルクオーテーションで囲って返してください
右に動かす場合は"2"左に動かす場合は"0"上に動かす場合は"3"下に動かす場合は"4"手前に動かす場合は"5"奥に動かす場合は"6"グリップを開く場合は"7"グリップを閉じる場合は"8"

返答例は以下とおりです
右に動かして右に動かします"2"
右に動かした後左に動かして右に動かします"2"左に動かします"0"
グリップを開いてグリップを開きます"7"

終了ストップ会話終了の意味なら EXIT のみ返してください
"""

conversation_history = [
    {"role": "system", "content": SYSTEM_PROMPT}
]

MAX_HISTORY = 10

def ask_llm(text):
    global conversation_history

    conversation_history.append({
        "role": "user",
        "content": text
    })

    if LLM_MODE == "api":
        from openai import OpenAI
        client = OpenAI(api_key=OPENAI_API_KEY)

        res = client.chat.completions.create(
            model=OPENAI_MODEL,
            messages=conversation_history
        )

        reply = res.choices[0].message.content

    else:
        res = requests.post(
            LOCAL_LLM_URL,
            json={
                "messages": conversation_history,
                "max_tokens": 512,
                "temperature": 0.6
            },
            timeout=60
        )

        reply = res.json().get("response", "")

    conversation_history.append({
        "role": "assistant",
        "content": reply
    })

    if len(conversation_history) > MAX_HISTORY:
        conversation_history = (
            [conversation_history[0]] +
            conversation_history[-(MAX_HISTORY - 1):]
        )

    # commands = re.findall(r'"([0-8])"', reply)
    commands = re.findall(r'"([02345678])"', reply)

    return {
        "reply": reply,
        "commands": commands
    }

config.py

# config.py
import os
from pathlib import Path
from dotenv import load_dotenv

# .envファイルから環境変数を読み込む
load_dotenv()

BASE_DIR = Path(__file__).resolve().parent

# ==========================================
# 1. LLM (推論エンジン) の設定
# ==========================================
# "api" にするとChatGPT"local" にするとローカルLLMを使用
LLM_MODE = os.getenv("LLM_MODE", "local")

# APIモード用 (ChatGPT)
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
OPENAI_MODEL = os.getenv("OPENAI_MODEL", "gpt-4o-mini")

# ローカルモード用 (FastAPI等で構築したGemma等のサーバー)
LOCAL_LLM_URL = os.getenv("LOCAL_LLM_URL", "http://127.0.0.1:8100/chat")
LOCAL_LLM_MODEL = os.getenv("LOCAL_LLM_MODEL", "gemma")

# ==========================================
# 2. 音声認識 (STT: faster-whisper) の設定
# ==========================================
WHISPER_MODEL = "small"
# GPUが使える場合は "cuda"使えない場合は "cpu" に変更
WHISPER_DEVICE = "cuda"
# GPUなら "float16"CPUなら "int8" を推奨
WHISPER_COMPUTE_TYPE = "float16"
WHISPER_SAMPLE_RATE = 16000

# ==========================================
# 3. 音声合成 (TTS: VOICEVOX) の設定
# ==========================================
VOICEVOX_URL = "http://127.0.0.1:50021"
VOICEVOX_SPEAKER_ID = 1  # ずんだもん等好きな話者IDに変更

# ==========================================
# 4. ロボットアーム (シリアル通信) の設定
# ==========================================
# Windowsなら "COM3" Mac/Linuxなら "/dev/ttyUSB0" 等に変更
SERIAL_PORT = "COM3"
SERIAL_BAUDRATE = 115200

.env

# ===== モード切り替え =====
# LLM_MODE=api
LLM_MODE=local

# ===== OpenAI =====
OPENAI_API_KEY=sk-proj-************************************************
OPENAI_MODEL=gpt-4o-mini

# ===== ローカルLLM =====
LOCAL_LLM_URL=http://127.0.0.1:8100/chat
LOCAL_LLM_MODEL=gemma

llm_server.py

# llm_server.py
# -*- coding: utf-8 -*-

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from llama_cpp import Llama
import uvicorn

# ===== 設定 =====
MODEL_PATH = r"【モデルを保存している場所のパスを記載】\gemma-2-9B-it-SimPO-Q5_K_M.gguf"
HOST = "127.0.0.1"
PORT = 8100

# ===== FastAPI =====
app = FastAPI()

print("[INFO] Loading LLM model...")

llm = Llama(
    model_path=MODEL_PATH,
    n_ctx=8000,
    n_gpu_layers=-1,
    n_batch=512,
    verbose=True,
    seed=0,
)

print("[INFO] LLM model loaded.")


# ===== リクエストモデル =====
class ChatRequest(BaseModel):
    messages: list
    max_tokens: int = 512
    temperature: float = 0.6


@app.get("/health")
def health():
    return {"status": "ok"}


@app.post("/chat")
def chat(req: ChatRequest):
    """
    OpenAI API風の messages 形式でローカルLLMに問い合わせるAPI
    """
    try:
        if not req.messages:
            raise HTTPException(status_code=400, detail="messages is empty")

        output = llm.create_chat_completion(
            messages=req.messages,
            max_tokens=req.max_tokens,
            temperature=req.temperature,
        )

        text = output["choices"][0]["message"]["content"]

        return {
            "response": text
        }

    except HTTPException:
        raise

    except Exception as e:
        raise HTTPException(
            status_code=500,
            detail=f"LLM generation failed: {str(e)}"
        )


if __name__ == "__main__":
    uvicorn.run(app, host=HOST, port=PORT)

LLM_robot_arm_arduino.ino

//-----サーボ制御用設定
#include <PCA9685.h> //PCA9685用ヘッダーファイル
PCA9685 pwm = PCA9685(0x40);    //PCA9685のアドレス指定

//-----各変数の設定
#define SERVOMIN 150            //最小パルス幅 
#define SERVOMAX 500            //最大パルス幅
#define servo_01 0              //サーボ1のピンの設定
#define servo_02 1              //サーボ2のピンの設定
#define servo_03 2              //サーボ3のピンの設定
#define servo_04 3              //サーボ4のピンの設定
#define servo_05 4              //サーボ5のピンの設定
#define servo_06 5              //サーボ6のピンの設定

double ini_arg_01 = 90;          // サーボの初期角度
double ini_arg_02 = 180;         // サーボの初期角度
double ini_arg_03 = 135;         // サーボの初期角度
double ini_arg_04 = 135;         // サーボの初期角度
double ini_arg_05 = 90;          // サーボの初期角度
double ini_arg_06 = 90;          // サーボの初期角度 ※シリアル通信版のinitial_positionに合わせる

double arg_01 = ini_arg_01;      // サーボの角度
double arg_02 = ini_arg_02;      // サーボの角度
double arg_03 = ini_arg_03;      // サーボの角度
double arg_04 = ini_arg_04;      // サーボの角度
double arg_05 = ini_arg_05;      // サーボの角度
double arg_06 = ini_arg_06;      // サーボの角度

double ARM_LEN2 = 105;           // アーム2の長さ105mm
double ARM_LEN3 = 100;           // アーム3の長さ100mm

//次の円筒座標位置は物理的に可能な位置か判定する 5mmの裕度を与える
double chiiki_MAX = pow((ARM_LEN2 - 5) + (ARM_LEN3 - 5), 2.0);
double chiiki_MIN = pow(ARM_LEN2 - ARM_LEN3, 2.0);

double r_now = 0;                // 円筒座標系の位置 r
double fai_now = 0;              // 円筒座標系の位置 fai
double pz_now = 0;               // 円筒座標系の位置 pz

int X_POS_L = 128;               // LスティックのX軸方向の読み取り値
int Y_POS_L = 128;               // LスティックのY軸方向の読み取り値
int X_POS_R = 128;               // RスティックのX軸方向の読み取り値
int Y_POS_R = 128;               // RスティックのY軸方向の読み取り値

int GRIP_servo5 = 128;           // サーボ5の入力(L1,R1)
int GRIP_L2 = 128;               // アームのグリップ部 閉まる方向
int GRIP_R2 = 128;               // アームのグリップ部 開く方向

int read_i = 0;
int count_i = 0;
int out_i = 0;
int arg_x = 0;

int serial_data = -1;
//-----サーボ制御end


//-----初期設定
void setup() {
  pwm.begin();                   //初期設定 (アドレス0x40用)
  pwm.setPWMFreq(50);            //PWM周期を50Hzに設定 (アドレス0x40用)        

  initial_position(arg_01, arg_02, arg_03, arg_04, arg_05, arg_06);  //ポジション初期化

  //pythonとシリアル通信
  Serial.begin(115200);
}

//-----メインループ
void loop() {

  //-----ロボットアームを動かす関数
  if (Serial.available() > 0) {
    serial_data = Serial.read();

    switch (serial_data) { 
      case '0':
        X_POS_L = 50;              
        break;
      case '2':
        X_POS_L = 200;   
        break;
      case '3':
        Y_POS_R = 50;   
        break;
      case '4':
        Y_POS_R = 200;   
        break;
      case '5':
        Y_POS_L = 50;   
        break;
      case '6':
        Y_POS_L = 200;   
        break;
      case '7':
        arg_06 = 45;     // 最大まで開く  
        break;
      case '8':
        arg_06 = 90;     // 初期位置へ戻す・閉じる
        break;
      default:        
        break; 
    }
  }
  //-----ロボットアームを動かす関数end

  //円筒座標系の位置計算  
  calculate_rfaipz(&r_now, &fai_now, &pz_now, arg_01, arg_02, arg_03);

  //円筒座標系の移動量Y_POS_L、Y_POS_Rからarg_02、arg_03を計算
  inverse_kinematics(Y_POS_L, Y_POS_R, r_now, pz_now, arg_02, arg_03, &arg_02, &arg_03);
    
  //-----サーボモーター用にコントローラー入力→角度_変換
  arg_01 = arg_01 + convert_01(X_POS_L, arg_01);
  //arg_02 = arg_02 + convert_02(Y_POS_L, arg_02);
  //arg_03 = arg_03 + convert_03(Y_POS_R, arg_03);
  arg_04 = arg_04 + convert_03(X_POS_R, arg_04);
  arg_05 = arg_05 + 20 * convert_01(GRIP_servo5, arg_05);
//  arg_06 = arg_06 + convert_04(GRIP_L2, GRIP_R2, arg_06);

  double r_pre = r_now;
  double pz_pre = pz_now;
  
  //円筒座標系の位置計算  
  calculate_rfaipz(&r_now, &fai_now, &pz_now, arg_01, arg_02, arg_03);

  //-----サーボ制御(関数呼び出し)
  servo_write(servo_01, arg_01);
  servo_write(servo_02, arg_02);
  servo_write(servo_03, arg_03);
  servo_write(servo_04, arg_04);
  servo_write(servo_05, arg_05);
  servo_write(servo_06, arg_06);

  //-----入力リセット
  X_POS_L = 128;      // LスティックのX軸方向の読み取り値
  Y_POS_L = 128;      // LスティックのY軸方向の読み取り値
  X_POS_R = 128;      // RスティックのX軸方向の読み取り値
  Y_POS_R = 128;      // RスティックのY軸方向の読み取り値

  GRIP_L2 = 128;      // アームのグリップ部 閉まる方向
  GRIP_R2 = 128;      // アームのグリップ部 開く方向
  GRIP_servo5 = 128;  // サーボ5
}
//-----メインループend

呼び出し関数部分です。
私は関数を別ファイルに作成していますが、同一ファイル内に作成しても問題ありません。

/////-----呼び出し用の関数まとめ-----/////

//-----サーボ出力用_サーボ番号、角度→出力_関数
void servo_write(int ch, double ang){ //動かすサーボチャンネルと角度を指定
  ang = map(ang, 0, 180, SERVOMIN, SERVOMAX); //角度(0~180)をPWMのパルス幅(150~500)に変換
  pwm.setPWM(ch, 0, ang);
}

//-----サーボ位置初期化用関数
void initial_position(double arg_01_x, double arg_02_x, double arg_03_x, double arg_04_x, double arg_05_x, double arg_06_x){

  //各サーボの初期値
  arg_01 = ini_arg_01;   // サーボの角度
  arg_02 = ini_arg_02;   // サーボの角度
  arg_03 = ini_arg_03;   // サーボの角度
  arg_04 = ini_arg_04;   // サーボの角度
  arg_05 = ini_arg_05;   // サーボの角度
  arg_06 = ini_arg_06;   // サーボの角度

  //サーボ出力
  servo_write(servo_01, arg_01);
  delay(500); 
  servo_write(servo_02, arg_02);
  delay(500); 
  servo_write(servo_03, arg_03);
  delay(500); 
  servo_write(servo_04, arg_04);
  delay(500); 
  servo_write(servo_05, arg_05);
  delay(500); 
  servo_write(servo_06, arg_06);
  delay(500); 
}

//-----リセット用関数
void software_reset() {
  asm volatile ("  jmp 0");  
} 

//-----サーボ1、サーボ5用_コントローラー入力→角度_変換関数
int convert_01(int input_x, double arg_x){
  if(arg_x == 170 && input_x > 190){      //サーボ角度制御、170度以下
    return 0;
  }
  else if (arg_x == 10 && input_x < 65){  //サーボ角度制御、10度以上
    return 0;
  } 
  else if(input_x > 190){     //2度ずつ増加
    return 1;
  } 
  else if(input_x < 65){      //2度ずつ減少
    return -1;
  }
  else{
    return 0;
  }
}

//-----サーボ2用_コントローラー入力→角度_変換関数
int convert_02(int input_x, double arg_x){
  if(arg_x == 180 && input_x > 190){      //サーボ角度制御、180度以下
    return 0;
  }
  else if (arg_x == 45 && input_x < 65){  //サーボ角度制御、45度以上
    return 0;
  } 
  else if(input_x > 190){     //1度ずつ増加
    return 1;
  } 
  else if(input_x < 65){      //1度ずつ減少
    return -1;
  }
  else{
    return 0;
  }
}

//-----(サーボ3×)、サーボ4〇用_コントローラー入力→角度_変換関数
int convert_03(int input_x, double arg_x){
  if(arg_x == 135 && input_x < 65){        //サーボ角度制御、135度以下
    return 0;
  }
  else if (arg_x == 0 && input_x > 190){   //サーボ角度制御、0度以上
    return 0;
  } 
  else if(input_x > 190){     //1度ずつ減少
    return -1;
  } 
  else if(input_x < 65){      //1度ずつ増加
    return 1;
  }
  else{
    return 0;
  }
}



//円筒座標系の座標位置を計算する関数
void calculate_rfaipz(double *r_now, double *fai_now, double *pz_now, double arg_x1, double arg_x2, double arg_x3){
  double r_x = 0;
  double fai_x = 0;
  double pz_x = 0;

  //サーボの角度を 度→ラジアンに変換
  double rad_arg_x1 = radians(arg_x1);
  double rad_arg_x2 = radians(arg_x2);
  double rad_arg_x3 = radians(arg_x3);

  r_x = ARM_LEN2 * cos(rad_arg_x2) + ARM_LEN3 * cos(rad_arg_x2 - rad_arg_x3);
  fai_x = arg_x1;
  pz_x = ARM_LEN2 * sin(rad_arg_x2) + ARM_LEN3 * sin(rad_arg_x2 - rad_arg_x3);

  *r_now = r_x;
  *fai_now = fai_x;
  *pz_now = pz_x;
}

//-----Y_POS_L、Y_POS_R用_コントローラー入力→円筒座標系のr軸に変換関数
int convert_02r(int input_x){
  if(input_x > 190){      //1mmずつ増加
    return -1;
  } 
  else if(input_x < 65){  //1mmずつ減少
    return 1;
  }
  else{
    return 0;
  }
}

//円筒座標系の逆運動学
void inverse_kinematics(double r_now_dx, double pz_now_dx, double r_now_x, double pz_now_x, double arg_02_x, double arg_03_x, double *arg_02, double *arg_03){

  //次の円筒座標位置からサーボモーター02,03の角度を求める
  double arg_02_next = arg_02_x;
  double arg_03_next = arg_03_x;

  //現時点での座標位置を保存
  double r_now_pre = r_now_x;
  double pz_now_pre = pz_now_x;
  
  //現在の円筒座標位置とコントローラーの入力から次の円筒座標位置を求める
  r_now_x = r_now_x + convert_02r(r_now_dx);
  pz_now_x = pz_now_x + convert_02r(pz_now_dx);

  //値域が制御範囲内か確認
  double chiiki_now = pow(r_now_x, 2.0) + pow(pz_now_x, 2.0);

  //制御範囲内でなければ移動しない
  if(chiiki_now > chiiki_MAX){
    r_now_x = r_now_pre;
    pz_now_x = pz_now_pre;

    arg_02_next = arg_02_x;
    arg_03_next = arg_03_x;
  }
  else if(chiiki_now < chiiki_MIN) {
    r_now_x = r_now_pre;
    pz_now_x = pz_now_pre;

    arg_02_next = arg_02_x;
    arg_03_next = arg_03_x;
  }
  else{
    //逆運動学の公式より
    arg_03_next = abs(acos((pow(r_now_x, 2.0) + pow(pz_now_x, 2.0) - (pow(ARM_LEN2, 2.0) + pow(ARM_LEN3, 2.0))) / (2 * ARM_LEN2 * ARM_LEN3)));

    double S3 = sin(arg_03_next);
    double C3 = cos(arg_03_next);

    arg_02_next = atan2(
      (ARM_LEN3 * S3 * r_now_x + (ARM_LEN2 + ARM_LEN3 * C3) * pz_now_x),
      ((ARM_LEN2 + ARM_LEN3 * C3) * r_now_x - ARM_LEN3 * S3 * pz_now_x)
    );

    //ラジアン→度に変換
    arg_02_next = degrees(arg_02_next);
    arg_03_next = degrees(arg_03_next);
  }

  //サーボ02用の角度 制御範囲内か確認
  if(arg_02_next > 180){
    *arg_02 = arg_02_x;
  }
  else if(arg_02_next < 0){
    *arg_02 = arg_02_x;
  }
  else{
    *arg_02 = arg_02_next;
  }

  //サーボ03用の角度 制御範囲内か確認
  if(arg_03_next > 135){
    *arg_03 = arg_03_x;
  }
  else if(arg_03_next < 0){
    *arg_03 = arg_03_x;
  }
  else{
    *arg_03 = arg_03_next;
  }
}

ロボットアームの作り方について

ロボットアームの作り方は下の記事で解説しています。

是非読んでみてください。

コスパ最強!? ロボットアームの作り方_その①
自作ロボットアームの紹介ロボットアームを自作したい方向けに、Arduinoを使った6軸ロボットアームの作り方をまとめました。本記事では、初心者でも再現できる構成で・必要な部品・組み立て手順・プログラム制御・よくあるトラブル(サーボが震える/…

コメント

タイトルとURLをコピーしました