生成AIとArduinoで作る姿勢矯正システム - 振動で教える猫背防止デバイス

こんにちは、Insight Edgeの小林まさみつです。本記事は Insight Edge Advent Calendar 2025 の8日目の記事です。 最近は生成AIをソフトウェア領域に応用した開発をしていますが、今回は趣向を変えてハードウェアと組み合わせたシステムを作成してみたので紹介します。

目次

1. はじめに

1.1 なぜ作ったのか

長時間のデスクワークで肩こりや腰痛に悩まされ、「姿勢を良くしなければ」と思いつつも、集中していると姿勢のことなど忘れてしまいます。

市販の姿勢矯正グッズも検討しましたが、高価なものが多く、効果も不透明。そこで「技術で解決できるのでは?」と考え、自作することにしました。

当初はエアバッグで物理的に姿勢を矯正する構想もありましたが、コストと複雑さを考慮し、振動で姿勢の悪化を通知するシンプルなシステムに方針転換しました。

従来の画像認識ライブラリでは、照明条件や服装の変化で精度が不安定になりがちです。一方、生成AIであれば自然言語で評価基準を定義でき、より柔軟な判定が期待できます。そこで生成AIで姿勢を評価し、振動で身体に直接フィードバックする実験的なシステムを構築しました。

1.2 完成システムの紹介

システムは以下の3つの要素で構成されています:

  • Webカメラ: 横から姿勢を撮影
  • PC: 生成AIで姿勢評価
  • Arduino+振動モーター: 姿勢の悪い箇所をユーザーに通知
    • Arduino とは、電子工作を簡単に始められる小型のコンピューター基板(=マイコン)です。

姿勢が悪くなると、該当する部位が振動して通知されます。例えば猫背なら腰が、左に傾いていれば左太ももが振動する仕組みです。 動作イメージは以下の通りです。

  1. Webカメラが5分ごとにユーザーの姿勢を撮影
    Webカメラから撮った座り姿勢
    左の画像は良い姿勢、右は悪い姿勢の例です。左足が浮いており、腰も前傾していることがわかります。右側のように姿勢が悪くなったタイミングで振動モーターを動作させます。
  2. 結果に応じてArduinoに指示を送り、振動モーターを動作

    結果に応じて、左足・右足・腰のいずれか、または複数が振動して姿勢の悪化を通知します。これを人の足にテープなどで固定して使用します。

1.3 この記事で分かること

本記事では、以下の内容を解説します:

  • 生成AI活用: AWS Bedrock Claude Sonnet 4による姿勢評価の実装方法
  • ハードウェア制御: Arduino + 振動モーターの制御回路の設計と配線
  • システム統合: Webカメラ ⟷ PC ⟷ Arduino間の通信の実装

2. システム概要

2.1 全体構成図

システムの全体像は以下の通りです。

システムアーキテクチャ

カメラで撮影した画像をPCで取得し、生成AIに送信して姿勢を評価。その結果に基づいてArduinoに指示を送り、適切な振動モーターを動作させます。

各コンポーネントは疎結合で、それぞれ独立してテスト・改善できる構成になっています。

2.2 使用技術スタック

システム全体で使用した技術は以下の通りです。

レイヤー 技術 用途
言語 Python 3.13 システム全体の制御
AI推論 AWS Bedrock
Claude Sonnet 4
姿勢評価
画像取得 OpenCV (Python) カメラ制御
通信 pySerial PC-Arduino間
マイコン Arduino Uno モーター制御

また、利用したツールは以下の通りです。

ツール 用途
Arduino IDE Arduinoコード開発/マイコンへの書き込み
Tinkercad 回路設計とシミュレーション
Webカメラ こちら を参考にiPhoneをWebカメラ化しました

2.3 動作の流れ

システムの動作フローは以下の通りです:

  1. 画像取得:Webカメラが5分ごとにユーザーの姿勢を撮影する
  2. AI評価: PythonでAWS Bedrock(Claude Sonnet 4)に画像を送信し、姿勢を評価
  3. コマンド送信:評価結果に応じてシリアル通信でArduinoに指示
    • 各部位のスコアをもとに、振動すべき箇所を3ビットで表現する
    • 左足・右足・腰の順番に0/1とし、問題がある箇所を 1 に設定
    • 例:000(全て良好)、100(左足が悪い)、111(全て悪い)
  4. モーター制御:Arduinoが該当する振動モーターを動作させる
  5. ループ:1に戻り、継続的に監視

評価頻度は5分に1回としました。 当初は30秒ごとの評価も検討しましたが、以下の理由で5分間隔を選択しました。

  • APIコストの削減
  • 姿勢改善には継続的な意識づけが重要で、頻繁すぎる通知は逆効果になるため

3. ハードウェア編:振動モーター制御回路

3.1 必要な部品リスト

回路の構築に用いた部品は以下の通りです。

部品名 型番・仕様 個数 用途
マイコン Arduino Uno 1 モーター制御
振動モーター FM34E 3 触覚フィードバック
トランジスタ DTC143EL 3 スイッチング
抵抗 1kΩ 3 ベース電流制限
ダイオード 1N4007 3 逆起電力保護
ブレッドボード 標準サイズ 1 配線用
ジャンパーワイヤー オス-オス 11本 回路の接続用
ジャンパーワイヤー オス-メス 6本 振動モーター接続用
ジャンパーワイヤー
(必要に応じて)
オス-メス 任意の本数 振動モーター延長用

振動モーターを延長する場合は、オス-メスのジャンパーワイヤーを追加で24本程度用意することを推奨します。

3.2 回路図と配線

回路図の作り方

回路設計の経験が無かったため、生成AIに回路図の作成を依頼しました。 出力された回路図をもとに、 Tinkercad でシミュレーションと配線図の作成をしました。 初心者でも簡単に始められ、無料で利用できるため非常に便利です。

基本回路(1個のモーター)

まず、1個の振動モーターを制御する基本回路を説明します。

1つのモーターを動作させる回路図

回路の動作原理:

Arduinoのデジタルピン(D3)から信号を出力し、抵抗を経由してトランジスタのベースに接続します。トランジスタはスイッチとして機能し、ベースに電圧がかかるとコレクタ-エミッタ間が導通し、モーターに電流が流れます。

ダイオードはモーターと並列に逆向きで接続され、モーター停止時の逆起電力を吸収します。これにより、Arduinoや他の回路が逆電圧で破損することを防ぎます。

PWM制御の必要性:

使用する振動モーターの定格は3Vですが、Arduinoの出力は5Vです。そのまま接続すると過電圧になるため、PWM(Pulse Width Modulation)制御を使って実効電力を調整します。具体的には、analogWrite(pin, 153) とすることで、約60%のデューティサイクル(153/255)で動作し、平均電圧を約3Vに下げることができます。

完全な配線(3個のモーター)

次に、3個のモーターを制御する完全な回路です。

3つのモーターを動作させる回路図

配線した回路

配線した回路

配線時の重要な注意点:

  1. トランジスタの向き:平らな面を手前に向けて挿入します。左からエミッタ(E)、コレクタ(C)、ベース(B)の順です。

  2. ダイオードの向き:銀色の帯のある方がカソード(+側)で、モーターのプラス側に接続します。逆に接続すると短絡の原因になります。

  3. PWM対応ピンの使用:Arduino Unoでは、D3/D5/D6/D9/D10/D11がPWM対応です。今回はD3/D5/D6を使用しています。

3.3 動作確認とコード

Arduino IDEのシリアルモニタから手動でコマンドを送信して、動作を確認できます。

Arduinoコード

// ピン定義
const int LEFT_LEG_PIN = 3;   // 左脚用振動モーター
const int RIGHT_LEG_PIN = 5;  // 右脚用振動モーター
const int WAIST_PIN = 6;      // 腰用振動モーター

// 振動設定
const int VIBRATION_DURATION = 5000;  // 振動時間(ミリ秒)
const int VIBRATION_INTENSITY = 153; // PWM値(0-255) 5V電源をモーターに約3Vで供給するため153に設定

String receivedData = "";
bool dataComplete = false;

void setup() {
  // シリアル通信を開始(9600 baud)
  Serial.begin(9600);

  // ピンを出力モードに設定
  pinMode(LEFT_LEG_PIN, OUTPUT);
  pinMode(RIGHT_LEG_PIN, OUTPUT);
  pinMode(WAIST_PIN, OUTPUT);

  // 初期状態:全モーター停止
  analogWrite(LEFT_LEG_PIN, 0);
  analogWrite(RIGHT_LEG_PIN, 0);
  analogWrite(WAIST_PIN, 0);

  // 起動確認用フラッシュ
  startupFlash();
}

void loop() {
  // シリアルデータが利用可能かチェック
  if (Serial.available()) {
    String receivedString = Serial.readString();
    if (receivedString.length() > 0) {
      receivedData = receivedString;
      dataComplete = true;
    }
  }

  // データ受信完了時の処理
  if (dataComplete) {
    processPostureFeedback(receivedData);
    receivedData = "";
    dataComplete = false;
  }
}

void processPostureFeedback(String data) {
  // 3桁のバイナリ文字列を期待(例:"101")
  if (data.length() != 3) {
    Serial.println("Error: Invalid data format. Expected 3 digits.");
    return;
  }

  // 各桁をチェックして対応する振動を制御
  bool leftLegVibrate = (data.charAt(0) == '1');
  bool rightLegVibrate = (data.charAt(1) == '1');
  bool waistVibrate = (data.charAt(2) == '1');

  // 振動パターンを実行
  executeVibrationPattern(leftLegVibrate, rightLegVibrate, waistVibrate);
}

void executeVibrationPattern(bool leftLeg, bool rightLeg, bool waist) {
  // 全モーター停止
  stopAllMotors();

  // 振動が必要な部位があるかチェック
  if (!leftLeg && !rightLeg && !waist) {
    return;
  }

  // 対象部位を振動
  if (leftLeg) {
    analogWrite(LEFT_LEG_PIN, VIBRATION_INTENSITY);
  }
  if (rightLeg) {
    analogWrite(RIGHT_LEG_PIN, VIBRATION_INTENSITY);
  }
  if (waist) {
    analogWrite(WAIST_PIN, VIBRATION_INTENSITY);
  }

  // 振動時間待機
  delay(VIBRATION_DURATION);

  // 全モーター停止
  stopAllMotors();
}

void stopAllMotors() {
  analogWrite(LEFT_LEG_PIN, 0);
  analogWrite(RIGHT_LEG_PIN, 0);
  analogWrite(WAIST_PIN, 0);
}

void startupFlash() {
  // 起動時に全モーターを短時間点灯してテスト
  Serial.println("System test - All motors flash");

  for (int i = 0; i < 3; i++) {
    analogWrite(LEFT_LEG_PIN, VIBRATION_INTENSITY);
    analogWrite(RIGHT_LEG_PIN, VIBRATION_INTENSITY);
    analogWrite(WAIST_PIN, VIBRATION_INTENSITY);
    delay(200);

    stopAllMotors();
    delay(200);
  }

  Serial.println("System test completed");
}


4. ソフトウェア編:姿勢判定システム

4.1 カメラ設置とPythonでの画像取得

前提:姿勢を表すためのモデル定義

以下のPydanticモデルを用いて、生成AIからの姿勢評価を受け取りArduinoに送信する形式へ変換します。

姿勢評価のPydanticモデルコード

from pydantic import BaseModel, Field

POSTURE_THRESHOLD = 0.7  # 姿勢スコアの閾値

class FeedbackModel(BaseModel):
    left_leg_score: float = Field(..., description="左脚の姿勢スコア", ge=0.0, le=1.0)
    right_leg_score: float = Field(..., description="右脚の姿勢スコア", ge=0.0, le=1.0)
    waist_score: float = Field(..., description="腰の姿勢スコア", ge=0.0, le=1.0)
    remarks: str = Field("", description="スコア評価の備考")

    def to_should_vibrate(self) -> str:
        """
        振動フィードバックが必要かどうかを判定
        3桁の0, 1の組み合わせで返す。
        左足・右足・腰の順番
        例:
        - 000: 全て良好
        - 100: 左足が悪い
        - 111: 全て悪い
        """
        res = ""
        res += "1" if self.left_leg_score < POSTURE_THRESHOLD else "0"
        res += "1" if self.right_leg_score < POSTURE_THRESHOLD else "0"
        res += "1" if self.waist_score < POSTURE_THRESHOLD else "0"
        return res

Pythonでの画像取得

OpenCVを使ってカメラから画像を取得します。

画像取得のコード

import cv2
from datetime import datetime

# Webカメラのキャプチャを開始
cap = cv2.VideoCapture(0)

# キャプチャがオープンしている間続ける
while(cap.isOpened()):
    ret, frame = cap.read()
    if ret:
        # カメラ映像を表示
        cv2.imshow('Webcam Live', frame)

        # 's'キーが押されたらスクリーンショットを保存
        if cv2.waitKey(1) & 0xFF == ord('s'):
            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
            filename = f"screenshot/screenshot_{timestamp}.png"
            cv2.imwrite(filename, frame)

        # 'q'キーが押されたらループから抜ける
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break
    else:
        break

# キャプチャをリリースし、ウィンドウを閉じる
cap.release()
cv2.destroyAllWindows()

ポイント:

  1. cv2.VideoCapture(0) の引数は環境によって異なります。複数のカメラが接続されている場合は、番号を変えて試してください。
  2. 動作確認用のため、's' キーでスクリーンショットを保存するようにしています。最終的には5分ごとに自動で保存するロジックを追加します。

4.2 生成AI(Bedrock Claude Sonnet 4)との連携

AWS Bedrockのセットアップ

AWS Bedrockを使用するには、事前にAWSアカウントとCLIの設定が必要です。

必要な準備: 1. AWSアカウントの作成 2. IAMユーザーの作成(Bedrock実行権限付与) 3. AWS CLIのインストールと設定(aws configure) 4. Bedrock APIへのアクセス権限の確認(リージョンは ap-northeast-1 など)

姿勢評価の実装

Claude Sonnet 4に画像を送信して姿勢を評価するクラスを実装します。

Claudeを呼び出し姿勢を評価するコード

import json
import boto3

from model import FeedbackModel

MODEL_ID = "" # Bedrockで使用するモデルIDを指定してください。

class BedrockClient():
    def __init__(self):
        self.client = boto3.client('bedrock-runtime')

    # 姿勢評価
    def evaluate_posture(self, img_bytes: bytes):
        system_prompt = (
            "ユーザーから提供されるbase64エンコードされた画像を解析し、姿勢評価を行ってください。\n"
            "画像横から撮影した人が映っています。座り姿勢が良いか悪いかを判定し、以下のJSON形式で返してください。\n\n"
            "1. left_leg_score: 左脚の姿勢スコア(0-1のfloat、1が最良)\n"
            "2. right_leg_score: 右脚の姿勢スコア(0-1のfloat、1が最良)\n"
            "3. waist_score: 腰の姿勢スコア(0-1のfloat、1が最良)\n\n"
            "JSON形式の例: {{\"left_leg_score\": 0.8, \"right_leg_score\": 0.9, \"waist_score\": 0.7}}"
        )
        messages = [{
            "role": "user",
            "content": [
                {
                    "image": {
                        "format": "png",
                        "source": {
                            "bytes": img_bytes
                        }
                    }
                }
            ]
        }]
        response = self.client.converse(
            modelId=MODEL_ID,
            system=[{"text": system_prompt}],
            inferenceConfig={"temperature": 0},
            messages=messages,
            toolConfig={
                "tools": [
                    {
                        "toolSpec": {
                            "name": "extract_Feedback_Model",
                            "description": "inputSchemaに沿ってFeedbackというPydanticモデルを出力するツール",
                            "inputSchema": {"json": FeedbackModel.model_json_schema()}
                        }
                    }
                ],
                "toolChoice": {
                    "tool": {
                        "name": "extract_Feedback_Model"
                    }
                }
            }
        )

        # Bedrockのレスポンスから姿勢評価結果のJSONを抽出し、FeedbackModelに変換して返す
        tool_res = response["output"]["message"]["content"][0]["toolUse"]["input"]
        if isinstance(tool_res, str):
            tool_res = json.loads(tool_res)
        return FeedbackModel(**tool_res)

ポイント

  1. ユーザーから提供される画像を読み取り、バイト列としてClaude Sonnet 4に送信します。
  2. BedrockにおけるClaudeはtoolConfigを利用することで、Pydanticモデルを用いたJSONレスポンスを受け取ることができます。一方、必ず意図した形式で返ってくるとは限らないため、必要に応じてエラーハンドリングを追加してください。

4.3 Arduino との通信(シリアル通信)

シリアル通信の基礎

PythonからArduinoに指示を送るには、シリアル通信を使用します。pySerialライブラリをインストールする必要があります。

Arduinoコントローラーの実装

姿勢評価結果に基づいてArduinoに適切なコマンドを送信するクラスを作成します。

Arduinoコントローラーのコード

import serial
from model import FeedbackModel


def run_vibration_feedback(arduino: serial.Serial, feedback: FeedbackModel) -> None:
    """
    Arduinoを使用して振動フィードバックを提供する関数
    """
    # 1. フィードバックを解析
    should_vibrate = feedback.to_should_vibrate()
    # 2. フィードバック信号をArduinoに送信
    arduino.write(bytes(should_vibrate, encoding="ascii"))

# 生成AIからのフィードバックをモック化
feedback = FeedbackModel(
    left_leg_score=0.4,
    right_leg_score=0.6,
    waist_score=0.7,
    remarks="Test feedback"
)

# Arduinoのシリアルポートとボーレートを設定。環境に応じて適切に変更してください。
ARDUINO_PORT = '/dev/cu.usbmodem114301'
ARDUINO_BAUDRATE = 9600

# Arduinoに接続する
arduino = serial.Serial(ARDUINO_PORT, ARDUINO_BAUDRATE)
# 振動フィードバックを実行
run_vibration_feedback(arduino, feedback)
# 接続を閉じる
arduino.close()

ポイント 1. 振動フィードバックの実行には、Arduinoが接続されている必要があります。 2. Arduinoのシリアルポートやボーレートは環境によって異なります。後述の確認方法を参考に適切に設定してください。

COMポートの確認方法:

  • Windows: デバイスマネージャーで「ポート(COMとLPT)」を確認。COM3, COM4 など。
  • Mac: ターミナルで ls /dev/cu.* を実行。/dev/cu.usbserial-XXXX など。
  • Linux: ターミナルで ls /dev/ttyUSB* または ls /dev/ttyACM* を実行。

Arduino IDEのシリアルモニタを開いている場合は、ポートが占有されているため、Pythonから接続できません。必ず閉じてから実行してください。また、ポート番号はArduino IDEで確認すると便利です。

4.4 統合プログラム

すべてのコンポーネントを統合し、システム全体を動作させるメインプログラムです。

コード全文

main.py

# main.py
from datetime import datetime
import cv2
import time
import serial
from bedrock import BedrockClient
from model import FeedbackModel

# Arduinoのシリアルポートとボーレートを設定。環境に応じて適切に変更してください。
ARDUINO_PORT = '/dev/cu.usbmodem114301'
ARDUINO_BAUDRATE = 9600

interval_sec = 300

def get_image(frame) -> bytes:
    """OpenCVのフレームを画像バイトに変換して返す"""
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    filename = f"screenshot/screenshot_{timestamp}.png"
    # スクリーンショットを保存
    cv2.imwrite(filename, frame)
    res: bytes = b""
    # 画像をバイトに変換
    with open(filename, "rb") as img_file:
        res = img_file.read()
    return res


def run_capture_analysis(frame): # -> Feedback:
    """
    OpenCVでキャプチャしたフレームを元に姿勢分析を行う関数
    """
    # 1. frameをbyteで送信
    img_bytes = get_image(frame)
    # 2. Bedrockに送信して姿勢推定
    bedrock_client = BedrockClient()
    return bedrock_client.evaluate_posture(img_bytes)

def run_vibration_feedback(arduino: serial.Serial, feedback: FeedbackModel) -> None:
    """
    Arduinoを使用して振動フィードバックを提供する関数
    """
    # 1. フィードバックを解析
    should_vibrate = feedback.to_should_vibrate()
    # 2. フィードバック信号を送信
    arduino.write(bytes(should_vibrate, encoding="ascii"))


def main():
    # ウェブカメラのキャプチャを開始
    cap = cv2.VideoCapture(0)
    arduino = serial.Serial(ARDUINO_PORT, ARDUINO_BAUDRATE)


    # キャプチャがオープンしている間続ける
    while(cap.isOpened()):
        time.sleep(interval_sec)
        ret, frame = cap.read()

        if not ret:
            break

        # デバッグ用にフレームを表示
        cv2.imshow('Webcam Live', frame)

        feedback = run_capture_analysis(frame)
        run_vibration_feedback(arduino, feedback)

        # 'q'キーが押されたらループから抜ける
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break

    # キャプチャをリリースし、ウィンドウを閉じる
    cap.release()
    cv2.destroyAllWindows()
    arduino.close()

if __name__ == "__main__":
    main()

bedrock.py

# bedrock.py
import json
import boto3

from model import FeedbackModel

MODEL_ID = "" # Bedrockで使用するモデルIDを指定

class BedrockClient():
    def __init__(self):
        self.client = boto3.client('bedrock-runtime')

    # 姿勢評価
    def evaluate_posture(self, img_bytes: bytes):
        system_prompt = (
            "ユーザーから提供されるbase64エンコードされた画像を解析し、姿勢評価を行ってください。\n"
            "画像横から撮影した人が映っています。座り姿勢が良いか悪いかを判定し、以下のJSON形式で返してください。\n\n"
            "1. left_leg_score: 左脚の姿勢スコア(0-1のfloat、1が最良)\n"
            "2. right_leg_score: 右脚の姿勢スコア(0-1のfloat、1が最良)\n"
            "3. waist_score: 腰の姿勢スコア(0-1のfloat、1が最良)\n\n"
            "JSON形式の例: {{\"left_leg_score\": 0.8, \"right_leg_score\": 0.9, \"waist_score\": 0.7}}"
        )
        messages = [{
            "role": "user",
            "content": [
                {
                    "image": {
                        "format": "png",
                        "source": {
                            "bytes": img_bytes
                        }
                    }
                }
            ]
        }]
        response = self.client.converse(
            modelId=MODEL_ID,
            system=[{"text": system_prompt}],
            inferenceConfig={"temperature": 0},
            messages=messages,
            toolConfig={
                "tools": [
                    {
                        "toolSpec": {
                            "name": "extract_Feedback_Model",
                            "description": "inputSchemaに沿ってFeedbackというPydanticモデルを出力するツール",
                            "inputSchema": {"json": FeedbackModel.model_json_schema()}
                        }
                    }
                ],
                "toolChoice": {
                    "tool": {
                        "name": "extract_Feedback_Model"
                    }
                }
            }
        )

        # Bedrockのレスポンスから姿勢評価結果のJSONを抽出し、FeedbackModelに変換して返す
        tool_res = response["output"]["message"]["content"][0]["toolUse"]["input"]
        if isinstance(tool_res, str):
            tool_res = json.loads(tool_res)
        return FeedbackModel(**tool_res)

model.py

# model.py
from pydantic import BaseModel, Field

POSTURE_THRESHOLD = 0.7  # 姿勢スコアの閾値

class FeedbackModel(BaseModel):
    left_leg_score: float = Field(..., description="左脚の姿勢スコア", ge=0.0, le=1.0)
    right_leg_score: float = Field(..., description="右脚の姿勢スコア", ge=0.0, le=1.0)
    waist_score: float = Field(..., description="腰の姿勢スコア", ge=0.0, le=1.0)
    remarks: str = Field("", description="スコア評価の備考")

    def to_should_vibrate(self) -> str:
        """
        振動フィードバックが必要かどうかを判定
        3桁の0, 1の組み合わせで返す。
        左足・右足・腰の順番
        例:
        - 000: 全て良好
        - 100: 左足が悪い
        - 111: 全て悪い
        """
        res = ""
        res += "1" if self.left_leg_score < POSTURE_THRESHOLD else "0"
        res += "1" if self.right_leg_score < POSTURE_THRESHOLD else "0"
        res += "1" if self.waist_score < POSTURE_THRESHOLD else "0"
        return res

motor.ino

// ピン定義
const int LEFT_LEG_PIN = 3;   // 左脚用振動モーター
const int RIGHT_LEG_PIN = 5;  // 右脚用振動モーター
const int WAIST_PIN = 6;      // 腰用振動モーター

// 振動設定
const int VIBRATION_DURATION = 5000;  // 振動時間(ミリ秒)
const int VIBRATION_INTENSITY = 153; // PWM値(0-255)

String receivedData = "";
bool dataComplete = false;

void setup() {
  // シリアル通信を開始(9600 baud)
  Serial.begin(9600);

  // ピンを出力モードに設定
  pinMode(LEFT_LEG_PIN, OUTPUT);
  pinMode(RIGHT_LEG_PIN, OUTPUT);
  pinMode(WAIST_PIN, OUTPUT);

  // 初期状態:全モーター停止
  analogWrite(LEFT_LEG_PIN, 0);
  analogWrite(RIGHT_LEG_PIN, 0);
  analogWrite(WAIST_PIN, 0);

  // 起動確認用フラッシュ
  startupFlash();
}

void loop() {
  // シリアルデータが利用可能かチェック
  if (Serial.available()) {
    String receivedString = Serial.readString();
    if (receivedString.length() > 0) {
      receivedData = receivedString;
      dataComplete = true;
    }
  }

  // データ受信完了時の処理
  if (dataComplete) {
    processPostureFeedback(receivedData);
    receivedData = "";
    dataComplete = false;
  }
}

void processPostureFeedback(String data) {
  // 3桁のバイナリ文字列を期待(例:"101")
  if (data.length() != 3) {
    Serial.println("Error: Invalid data format. Expected 3 digits.");
    return;
  }

  // 各桁をチェックして対応する振動を制御
  bool leftLegVibrate = (data.charAt(0) == '1');
  bool rightLegVibrate = (data.charAt(1) == '1');
  bool waistVibrate = (data.charAt(2) == '1');

  // 振動パターンを実行
  executeVibrationPattern(leftLegVibrate, rightLegVibrate, waistVibrate);
}

void executeVibrationPattern(bool leftLeg, bool rightLeg, bool waist) {
  // 全モーター停止
  stopAllMotors();

  // 振動が必要な部位があるかチェック
  if (!leftLeg && !rightLeg && !waist) {
    return;
  }

  // 対象部位を振動
  if (leftLeg) {
    analogWrite(LEFT_LEG_PIN, VIBRATION_INTENSITY);
  }
  if (rightLeg) {
    analogWrite(RIGHT_LEG_PIN, VIBRATION_INTENSITY);
  }
  if (waist) {
    analogWrite(WAIST_PIN, VIBRATION_INTENSITY);
  }

  // 振動時間待機
  delay(VIBRATION_DURATION);

  // 全モーター停止
  stopAllMotors();
}

void stopAllMotors() {
  analogWrite(LEFT_LEG_PIN, 0);
  analogWrite(RIGHT_LEG_PIN, 0);
  analogWrite(WAIST_PIN, 0);
}

void startupFlash() {
  // 起動時に全モーターを短時間点灯してテスト
  Serial.println("System test - All motors flash");

  for (int i = 0; i < 3; i++) {
    analogWrite(LEFT_LEG_PIN, VIBRATION_INTENSITY);
    analogWrite(RIGHT_LEG_PIN, VIBRATION_INTENSITY);
    analogWrite(WAIST_PIN, VIBRATION_INTENSITY);
    delay(200);

    stopAllMotors();
    delay(200);
  }

  Serial.println("System test completed");
}

プログラムの構成:

  1. 初期化フェーズ:カメラとArduinoを初期化
  2. メインループ
    • 5分待機
    • カメラから画像を取得
    • AIで姿勢評価
    • Arduinoにコマンド送信
  3. 終了処理:終了時リソースを適切に解放

5. 実際に使ってみて

システムを1週間使用した結果、以下のような効果と課題が見えてきました。

5.1 効果を実感した点

最初はほぼ毎回振動させられ、そのたびに「ハッとして」姿勢を直していました。振動というフィードバックは、視覚や聴覚と比べて邪魔にならず、作業を中断させない点が良かったです。

4日目くらいから、少しずつ姿勢が改善され、振動される頻度が減ってきました。座り姿勢を意識する習慣がついたように感じます。

5.2 振動の強さについて

使用した3V振動モーターは、服の上からでもはっきりと分かるため、通知としての役割は十分果たせています。

一方で、長時間使用すると慣れてしまい、振動に対する感度が下がる傾向も見られました。振動パターンをランダム化する、または強弱をつけるなどの工夫が必要かもしれません。

5.3 生成AIの判定精度

Claude Sonnet 4の姿勢評価は想像以上に精密で、「確かにその通り」と思える判定が多くありました。

特に、微妙な体の傾きや脚の位置なども的確に指摘してくれるため、自分では気づかない姿勢の問題を発見できました。


6. 今後の展望

このシステムは実験的なプロトタイプですが、以下のような発展性があります。

  • 複数部位への拡張: 現在は3箇所ですが、首、肩、背中など5〜7箇所に増やすことで、より細かい姿勢の矯正が可能になります。
  • モーターの種類変更: 振動モーター以外に、エアポンプとエアバッグを利用することで良い姿勢に矯正させることができます。
  • バッテリー駆動化: USB給電からバッテリー駆動に変更し、ケーブルレスで使用できるようにできます。

同様の「自然言語での評価基準定義 + 物理フィードバック」のパターンは、評価が主観的になりがちな他の分野でも参考になるかもしれません。


7. まとめ

本記事を書くにあたり、大学以来久しぶりに電子工作をしました。生成AIを活用して回路図を作成し、シミュレータで動作確認をすることで、初学者でも簡単に取り組めました。ソフトウェアアプリケーションの領域だけでなく、ハードウェア制御の分野にも生成AIを活用できることを実感し、自身で取り組める範囲が広がったと感じています。本記事が、同様の課題を持つ方々の参考になれば幸いです。