8
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

うごけ!モータ!(副題:MCPサーバを使ってモータを雑に回す)

Last updated at Posted at 2025-09-06

はじめに

本記事では、MCP(Model Context Protocol)サーバを経由して、雑に(すなわち自然言語で)モータを動作させるデモを紹介します。MCPサーバやさまざまなコンポーネントの連動によって、現実世界のモータを自然言語でかんたんに駆動することができます。

↓こんな動きをします。

なお本記事の内容は、下記のMCPサーバ経由でラジコン用サーボを動かすものを、Roller485をI2C接続で動かすものに改変したものです。

実行環境

  • Win10/11
  • VSCode
  • GitHub Copilot
  • Node.js 22.x
  • Arduino UNO 互換ボード
  • Roller485モータユニット(位置制御モードを使用)

主要なコンポーネントとその接続

  • index.js: MCPサーバ本体。roller485-mcpサーバのmoveRoller485 ツールで角度指定(-3600〜3600度)によるモータ制御を行う
  • MCPツール: moveRoller485(引数 degrees: number)
  • Roller485: STM32マイコンが内蔵された、ブラシレスDCモータユニット
  • Johnny-Five: MCPサーバ(JSコード)にて、ArduinoボードとのIFとして機能するAPI
  • Firmata: Arduinoボードに書き込んで、MCPサーバ(JSコード)とのIFとして機能するファームウェア
  • I2C通信: ArduinoボードとRoller485を接続

env_light.png

セットアップ~動かすまでの手順

  1. Arduino互換ボードをUSB接続、Roller485をI2C接続(激しく動かす・負荷を繋ぐ場合には5Vでは心許ないので、HT3.96 コネクタで6~16Vを供給)

  2. Arduino側をNode.js経由で動かすために、Firmataを書き込む。 Arduino IDEを起動して、ファイル>スケッチの例>Firmata>StandardFirmata を選択し、Arduinoに書き込む。
    image.png

  3. 依存パッケージをPCにインストール

    npm install
    
  4. MCPサーバを起動

    npm index.js
    

    もしくは、VS Codeの場合は下記の手順でもMCPサーバが起動できる。
    4.1. .vscode/mcp.jsonに以下を設定する。

    mcp.json
     {
     "servers": {
         "roller485-mcp": {
         "type": "stdio",
         "command": "node",
         "args": [
             "yourworkspace\\index.js"
         ]
         }
     }
     }
    

    4.2. コマンドパレット(Ctrl+Shift+P)で「MCP: Start Server」を実行し、roller485-mcpを選択する。歯車マークからサーバーの起動、を選択。
    image.png

    正常な場合、MCPサーバの起動時のログは下記のようになる。ツールのdescriptionが見当たらない…とワーニングが出るが、無視してOK。

    [info] サーバー roller485-mcp を起動しています
    [info] 接続状態: 開始しています
    [info] Starting server from LocalProcess extension host
    [info] 接続状態: 開始しています
    [info] 接続状態: 実行中
    [warning] [server stderr] [mcp] Board ready
    [warning] [server stderr] [mcp] MCP server started (stdio)
    [info] Discovered 1 tools
    [warning] Tool moveRoller485 does not have a description. Tools must be accurately described to be called
    
  5. GitHub CopilotのチャットをAgentモードで起動。MCPサーバとツールが認識されていることを確認。
    copilot_tool_check.png

  6. チャット欄に「moveRoller485でモータを〇〇度回して」、「適当な角度でモータを回して」などを入力する。問題なくMCPサーバが起動していれば、詳しい使い方自体もチャットで聞ける。
    image.png

  7. ツールの実行前に確認ウインドウが表示されるので、続行を押すと指定した回転角度でRoller485が回転する。応答がイマイチ要領を得ない場合は、VSCodeの再起動、ウインドウの再起動(コマンドパレットを開き、>Reload Windowを選択)、新しいチャットを開く…などを試す。

MCPツールの使い方

  • moveRoller485 は degrees(-3600〜3600)で回転角度を指定
  • 1度 = 100counts で内部変換
  • 目標位置到達判定は150ms経過後、誤差50counts以内に達したとき行う
  • タイムアウトは10秒
  • 結果は OK pos=xxxx (deg=xxx) または Timeout pos=xxxx targetCounts=xxxx (deg=xxx)

MCPサーバのコード

このコードでは、先述のとおりツールのdescriptionが見当たらない…とワーニングが出ます。最近の記法ではserver.registerTool…を使うのが普通みたいで、このあたりの詳細はあまり気にしないこととします。

index.js

// index.js — MCP + Johnny-Five + Roller485
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
import five from 'johnny-five';

// ===== Roller485固定パラメータ =====
const I2C_ADDR     = 0x64;  // Roller485 I2C アドレス
const REG_ENABLE   = 0x00;  // モータイネーブルレジスタアドレス
const REG_MODE     = 0x01;  // モード設定レジスタアドレス
const MODE_POS     = 0x02;  // 位置モード
const REG_POS_TGT  = 0x80;  // 目標位置設定レジスタアドレス s32 LE
const REG_POS_NOW  = 0x90;  // 現在位置読み出しレジスタアドレス s32 LE
const DEG_TO_CNT   = 100;   // 1degあたり100counts

// 到達判定のパラメータ
const WAIT_ENABLE_MS_STABLE = 150;  // 誤差内に留まる最短時間[ms]
const WAIT_POLL_MS          = 20;   // ポーリング周期[ms]
const WAIT_EPS_COUNTS       = 50;   // 許容誤差[counts]
const WAIT_TIMEOUT_MS       = 10000;// タイムアウト[ms]

// ===== ユーティリティ =====
const s32le = (v) => {
  const n = (v | 0) >>> 0;
  return [n & 0xFF, (n >> 8) & 0xFF, (n >> 16) & 0xFF, (n >> 24) & 0xFF];
};
const fromS32le = (b) => (b[0] | (b[1] << 8) | (b[2] << 16) | (b[3] << 24)) | 0;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

// 角度[deg] → 絶対カウント
const degToCounts = (deg) => Math.round(deg * DEG_TO_CNT); // Roller485の場合、1degあたり100counts

// ===== メイン =====
async function main() {
  // Johnny-Five 初期化
  const board = new five.Board({ repl: false, debug: false });
  await new Promise((res) => {
    board.on('ready', () => {
      board.i2cConfig(); // 既定100kHz
      console.error('[mcp] Board ready');
      res();
    });
  });

  const writeBytes = (reg, bytes) => board.i2cWrite(I2C_ADDR, [reg, ...bytes]);
  const readS32Once = (reg) =>
    new Promise((resolve, reject) => {
      // レジスタ指定で4バイト読む
      board.i2cReadOnce(I2C_ADDR, reg, 4, (data) => {
        if (!data || data.length !== 4) return reject(new Error('I2C read length'));
        resolve(fromS32le(data));
      });
    });

  // MCP サーバ
  const server = new McpServer({
    name: 'roller485-mcp',
    version: '1.1.0',
    description: 'Roller485 を I2C で角度指定で回す。指定可能範囲は-3600〜3600度',
  });

  server.tool(
    'moveRoller485',
    {
      degrees: z.number().min(-3600).max(3600),// -3600〜3600度
    },
    async ({ degrees }) => {
      try {
        // 角度 -> カウント
        const targetCounts = degToCounts(degrees);

        // 位置モード+Enable(冪等)
        writeBytes(REG_MODE,   [MODE_POS]);
        writeBytes(REG_ENABLE, [1]);

        // 目標位置書き込み
        writeBytes(REG_POS_TGT, s32le(targetCounts));

        // 固定条件で到達待ち
        const t0 = Date.now();
        let stableStart = null;
        let lastPos = 0;

        while (true) {
          const pos = await readS32Once(REG_POS_NOW);
          lastPos = pos;
          const err = Math.abs(targetCounts - pos);

          if (err <= WAIT_EPS_COUNTS) {
            if (stableStart === null) stableStart = Date.now();
            if (Date.now() - stableStart >= WAIT_ENABLE_MS_STABLE) {
              return {
                content: [{ type: 'text', text: `OK pos=${pos} (deg=${degrees})` }],
              };
            }
          } else {
            stableStart = null;
          }

          if (Date.now() - t0 > WAIT_TIMEOUT_MS) {
            return {
              content: [{ type: 'text', text: `Timeout pos=${lastPos} targetCounts=${targetCounts} (deg=${degrees})` }],
              isError: true,
            };
          }

          await sleep(WAIT_POLL_MS);
        }
      } catch (e) {
        console.error('[mcp] setRollerAngleDeg error:', e);
        return {
          content: [{ type: 'text', text: `Error: ${String(e)}` }],
          isError: true,
        };
      }
    }
  );

  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error('[mcp] MCP server started (stdio)');
}

main().catch((e) => console.error('[mcp] fatal:', e));

依存パッケージ

  • @modelcontextprotocol/sdk : MCPサーバ
  • johnny-five : Arduino制御
  • zod : パラメータバリデーション

おわりに

本記事では、MCPサーバなどによって、自然言語で簡単にモータが動かせることを紹介しました。自然言語で簡単に現実環境に働きかけできると、今後さまざまな応用が考えられます。

ただし、例えば「ちょっとだけ動け」と言っても、その意図を正確にCopilot Chatへ伝えるのはやはり難しいです。動画内でも「現在角度から相対的に、少しだけ回転してほしい」という意図が伝わらず、「絶対角度として小さな角度(例えば15°)にする」ために、相対的にはけっこう大きな回転をしたりします。また、チャット一つでモノが動くことは便利な一方、安全面が非常に重要なことは想像に難くないです。そのあたりの技術も今後に期待ですね。

間違い、ご指摘等あればぜひお知らせください。閲覧いただきありがとうございました。

8
6
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?