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サーババージョンを作るのも面白いかも
どなたかの参考となりましたら幸いです。
参考
- ROS2 Jazzy 公式インストールガイド
- rpi-lgpio ドキュメント
- 前作記事:ラズパイロボット自作記(2025.11)
- kuro 著 技術評論社「RaspberryPiでロボット制作(コミュニケーションロボットSIROの制作日誌)」