0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ラズパイロボットをClaude Codeを使ってROS2化した記録(2026.05)

0
Last updated at Posted at 2026-05-04

1. はじめに

前回の記事(ラズパイロボット自作記(2025.11))では、Raspberry Pi5を使ってコミュニケーションロボットを自作し、一応音声による操作できるところまで実装しました。(精度が低く、一応レベルです)

今回は、そのロボットを**ROS2(Robot Operating System 2)**化した記録です。

ROS2を採用した理由は、ロボット開発の標準プラットフォームを学びたかったこと、そして将来的に自律動作や複数センサーの統合をしやすくするためです。

さらに、コーディングには文明の利器 Claude Code を活用しました。
この記事の素案もClaude Codeに書いてもらいました。
本当にすごい時代になったものです!

2. 環境

項目 内容
ハードウェア Raspberry Pi 5
OS Raspberry Pi OS (64-bit)
ROS2バージョン Jazzy Jalisco
コーディング支援 Claude Code

3. ROS2パッケージの構成

今回作成したROS2パッケージ SIRO_ros のノード構成は以下のとおりです。

SIRO_ros/
├── siro_move.py          # モータ制御ノード
├── siro_cpu_temp.py      # CPU温度モニタリングノード
├── siro_localipaddress.py # ローカルIPアドレス通知ノード
├── siro_ondo.py          # 室温センサーノード
├── siro_uss.py           # 超音波センサー(距離計測)ノード
├── siro_opencv_camera.py # カメラノード(OpenCV)
├── siro_whisper_server.py # 音声認識サーバーノード(Whisper)
└── siro_whisper_client.py # 音声認識クライアントノード

setup.py のエントリーポイントは以下のように定義しています。

entry_points={
    'console_scripts': [
        'siro_move = SIRO_ros.siro_move:main',
        'cpu_temp = SIRO_ros.siro_cpu_temp:main',
        'local_ipaddress = SIRO_ros.siro_localipaddress:main',
        'ondo = SIRO_ros.siro_ondo:main',
        'uss = SIRO_ros.siro_uss:main',
        'camera = SIRO_ros.siro_opencv_camera:main',
        'whisper_server = SIRO_ros.siro_whisper_server:main',
        'whisper_client = SIRO_ros.siro_whisper_client:main',
    ],
},

4. モータ制御の改善:speed/duration → linear/angular

ROS2化にあたって、モータ制御のインターフェースを大きく変更しました。

変更前:speed と duration による制御

前作では、速さ(speed)と継続時間(duration)を指定してモータを動かすシンプルな方式でした。

# 前作のモータ制御イメージ
move(speed=0.5, duration=1.0)  # 0.5の速さで1秒間移動

この方式では「左カーブ」のような曲線的な動きが実現できず、直進・後退・その場回転の4動作のみでした。

変更後:linear と angular による差動駆動制御

ROS2化後は、ROS標準のTwistメッセージを使い、linear.x(直進速度) と angular.z(回転速度) で制御するようにしました。

from geometry_msgs.msg import Twist

# Twistメッセージで制御
msg = Twist()
msg.linear.x = 0.5   # 前進速度
msg.angular.z = 0.3  # 左回転速度

左右モーターの速度は、差動駆動の混合式で計算します。

left  = linear.x - angular.z
right = linear.x + angular.z

操作パターンの比較

操作 linear.x angular.z 左モーター 右モーター
前進 +0.5 0.0 +0.5 +0.5
後退 -0.5 0.0 -0.5 -0.5
左回転 0.0 +0.5 -0.5 +0.5
右回転 0.0 -0.5 +0.5 -0.5
左カーブ +0.5 +0.3 +0.2 +0.8

前作では不可能だった左カーブ・右カーブのような曲線移動が、linear/angular制御にすることで自然に実現できるようになりました。これはROS2化の大きなメリットの一つです。

ハマりポイント:linear と angular による差動駆動制御は RPi.GPIO ではできない。

GPIOの制御には RPi.GPIO を使っていました。

import RPi.GPIO as GPIO

ところがRaspberry Pi5は、GPIOコントローラーが従来のBCM2711からRP1チップに変わっており、RPi.GPIOが対応していません。実行してもモータが全く動きません。

解決策:rpi-lgpioへの移行

rpi-lgpioパッケージをインストールしました。

pip install rpi-lgpio

コードの変更箇所は、import文で、RIi.GPIOを lgpioに変更しただけです。

6. Claude Codeを活用したコード生成

今回の開発では、Claude Codeをコード生成に活用しました。

活用方法

既存のテストコード(test_motor.py)を参照させた上で、本番コードの実装を依頼しました。

プロンプト例:

test_motor.pyを参考に、main.pyにロボットの移動コマンドを実装してほしい。

Claude Codeはコードの文脈を理解した上で、ROS2のノード構造に合わせたコードを生成してくれました。

よかった点

  • コードの実装例をググる時間が大幅に短縮した
  • ROS2の package.xml や setup.pyの更新が煩わしくない
  • 既存コードのスタイルや変数名の命名規則を引き継いだコードを生成してくれる
  • エラーメッセージを貼り付けると原因と修正方法を提示してくれる

注意が必要だった点

  • それらしいコードが生成されているが、実行するとエラーになったり、エラーにならなくても動かないコードが生成される場合もあります

7. 各ノードの概要

siro_move(モータ制御ノード)

ロボットの移動を制御するノードです。サブスクライブするトピックからコマンドを受け取り、rpi-lgpio経由でモータドライバーにPWM信号を送ります。

import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist
import lgpio

# GPIO pin assignments (BCM numbering)
PIN_A_ENBL  = 20   # DRV8835 AENBL  – Motor A enable / PWM speed
PIN_A_PHASE = 21   # DRV8835 APHASE – Motor A direction
PIN_B_ENBL  = 12   # DRV8835 BENBL  – Motor B enable / PWM speed
PIN_B_PHASE = 16   # DRV8835 BPHASE – Motor B direction

PWM_FREQ = 1000    # Hz


class SiroMove(Node):
    def __init__(self):
        super().__init__('siro_move')

        self._setup_gpio()

        self.create_subscription(Twist, 'cmd_vel', self._cmd_vel_cb, 10)
        self.get_logger().info('siro_move node started')

    def _setup_gpio(self):
        self._h = lgpio.gpiochip_open(4)

        for pin in (PIN_A_ENBL, PIN_A_PHASE, PIN_B_ENBL, PIN_B_PHASE):
            lgpio.gpio_claim_output(self._h, pin, 0)

        lgpio.tx_pwm(self._h, PIN_A_ENBL, PWM_FREQ, 0)
        lgpio.tx_pwm(self._h, PIN_B_ENBL, PWM_FREQ, 0)

    def _set_motor(self, enbl_pin, phase_pin, speed: float):
        """speed: -1.0 (full reverse) to +1.0 (full forward), 0 = stop."""
        speed = max(-1.0, min(1.0, speed))
        if speed >= 0:
            lgpio.gpio_write(self._h, phase_pin, 1)
            lgpio.tx_pwm(self._h, enbl_pin, PWM_FREQ, speed * 100.0)
        else:
            lgpio.gpio_write(self._h, phase_pin, 0)
            lgpio.tx_pwm(self._h, enbl_pin, PWM_FREQ, abs(speed) * 100.0)

    def _cmd_vel_cb(self, msg: Twist):
        linear  = msg.linear.x
        angular = msg.angular.z

        left  = linear - angular
        right = linear + angular

        max_val = max(abs(left), abs(right), 1.0)
        left  /= max_val
        right /= max_val

        self.get_logger().info(f'cmd_vel: linear={linear:.2f} angular={angular:.2f} → left={left:.2f} right={right:.2f}')

        self._set_motor(PIN_A_ENBL, PIN_A_PHASE, left)
        self._set_motor(PIN_B_ENBL, PIN_B_PHASE, right)

    def destroy_node(self):
        lgpio.tx_pwm(self._h, PIN_A_ENBL, PWM_FREQ, 0)
        lgpio.tx_pwm(self._h, PIN_B_ENBL, PWM_FREQ, 0)
        lgpio.gpiochip_close(self._h)
        super().destroy_node()


def main(args=None):
    rclpy.init(args=args)
    node = SiroMove()
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        pass
    finally:
        node.destroy_node()
        rclpy.shutdown()


if __name__ == '__main__':
    main()

siro_whisper_server(音声認識ノード)

OpenAIのWhisperを使った音声認識をROS2のサービスとして実装しました。tinyを載せましたが、音声認識率は悪そうです。(なお、small以上のモデルは遅延が大きく、実用的ではないと感じました)

import os
import tempfile

import numpy as np
import sounddevice as sd
import scipy.io.wavfile as wavfile
from faster_whisper import WhisperModel

import rclpy
from rclpy.node import Node
from rclpy.action import ActionServer, CancelResponse, GoalResponse

from siro_interfaces.action import Transcribe

SAMPLE_RATE   = 16000   # Whisper が期待するサンプルレート
DEFAULT_MODEL = 'tiny'  # tiny / base / small(Pi 5 では tiny or base 推奨)


class WhisperServer(Node):
    def __init__(self):
        super().__init__('whisper_server')

        model_name = self.declare_parameter('model', DEFAULT_MODEL).value
        self.get_logger().info(f'Loading Whisper model: {model_name} ...')
        # CPU + int8 量子化で Pi 5 でも動作する
        self._model = WhisperModel(model_name, device='cpu', compute_type='int8')
        self.get_logger().info('Whisper model loaded.')

        self._action_server = ActionServer(
            self,
            Transcribe,
            'transcribe',
            execute_callback=self._execute_cb,
            goal_callback=lambda _: GoalResponse.ACCEPT,
            cancel_callback=lambda _: CancelResponse.ACCEPT,
        )
        self.get_logger().info('whisper_server started')

    def _execute_cb(self, goal_handle):
        duration = float(goal_handle.request.duration)
        self.get_logger().info(f'Recording {duration:.1f}s ...')

        # 録音
        total_samples = int(duration * SAMPLE_RATE)
        chunk_samples = SAMPLE_RATE // 10   # 0.1秒ごとに読み取り
        recorded      = np.zeros(total_samples, dtype=np.float32)
        idx           = 0

        with sd.InputStream(samplerate=SAMPLE_RATE, channels=1, dtype='float32') as stream:
            while idx < total_samples:
                if goal_handle.is_cancel_requested:
                    goal_handle.canceled()
                    result = Transcribe.Result()
                    result.text    = ''
                    result.success = False
                    return result

                read_n      = min(chunk_samples, total_samples - idx)
                data, _     = stream.read(read_n)
                recorded[idx:idx + read_n] = data[:read_n, 0]
                idx        += read_n

                elapsed  = idx / SAMPLE_RATE
                feedback = Transcribe.Feedback()
                feedback.elapsed = elapsed
                feedback.status  = f'録音中 {elapsed:.1f} / {duration:.1f} s'
                goal_handle.publish_feedback(feedback)

        # 一時 WAV ファイルに書き出して Whisper で認識
        self.get_logger().info('Transcribing ...')
        with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as f:
            tmp_path = f.name
            wavfile.write(tmp_path, SAMPLE_RATE, recorded)

        segments, _ = self._model.transcribe(tmp_path, language='en')
        text = ''.join(seg.text for seg in segments).strip()
        os.unlink(tmp_path)
        self.get_logger().info(f'Result: {text}')

        goal_handle.succeed()
        result         = Transcribe.Result()
        result.text    = text
        result.success = True
        return result

    def destroy_node(self):
        self._action_server.destroy()
        super().destroy_node()


def main(args=None):
    rclpy.init(args=args)
    node = WhisperServer()
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        pass
    finally:
        node.destroy_node()
        rclpy.shutdown()


if __name__ == '__main__':
    main()


8. ビルドと実行

パッケージのビルド

cd ~/ros2_ws
colcon build --packages-select SIRO_ros
source install/setup.bash

ノードの起動

各ノードは個別に起動できます。

# モータ制御ノードの起動
ros2 run SIRO_ros siro_move

# 音声認識サーバーの起動
ros2 run SIRO_ros whisper_server

9. まとめと今後の展望

今回の作業をまとめると以下のとおりです。

作業 内容
ROS2化 Jazzy JaliscoをRaspberry Pi5に導入し、全機能をROS2ノードとして再実装
モータ制御の改善 speed/duration → linear/angularの差動駆動制御に変更、曲線移動が可能に
GPIO対応 RPi.GPIO → rpi-lgpioへ移行(Pi5のRP1チップ対応)
ノード作成 モータ・カメラ・センサー・音声認識の8ノード
コード生成 Claude Codeを活用

Claude Codeの活用
「test_motor.pyを参考に実装してほしい」という自然言語の指示だけで、既存コードのスタイルを踏まえた実装を提示してくれるのは、従来の開発と比べて大きな時短になりました。

今後の展望としては、以下を検討しています。

  • 音声認識の改善
  • LLMによるコントロール
  • バッテリー駆動への対応(前作からの課題)
  • MCPサーババージョンを作るのも面白いかも

どなたかの参考となりましたら幸いです。


参考

0
1
0

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
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?