Bu, konunun uzun ve teknik rehberi (ESP-IDF, MicroPython, rclcpp gibi alternatiflerle). Adım adım yol için Porti görevlerine dön.

Ana rota · Hafta 3 · 10–18 saat

Yürüyen kutu

Şasi bas, iki motor, odometry, kalibrasyon

İki motor, basılmış bir şasi, sökülebilir bir sarhoş teker. Tekerlek sayımlarından robotun nerede olduğunu hesaplayan ilk C++ ROS 2 node'unu yazıyorsun, hatasını metreyle ölçüyor ve UMBmark ile düzeltiyorsun. Şasi iki modlu tasarlanıyor: aynı gövde 4. haftada dengede duracak.

Günün sonunda elinde
  • Mekanik sayfasında tasarlanıp basılmış iki modlu şasi
  • ESP32: /cmd_vel → diferansiyel sürüş kinematiği → iki PID
  • URDF / xacro, RViz'de robot ve TF ağacı
  • C++ odometry node'u: /odom + odom→base_footprint
  • UMBmark ile kalibrasyon: önce/sonra kapanma hatası (cm)
3.1

Şasiyi tasarla ve bas

Basılmış alt plaka, motor kelepçeleri, iki tekerlek ve pil yuvası; motorlar takılı, sarhoş teker yerinde. Robot masada duruyor.

Yap
Kanıt: Montajlı robotun fotoğrafı ve ölçülen değerler: tekerlek çapı, iz genişliği, sarhoş teker yüksekliği.
Tuzaklar
  • Buck'ı ayarlamadan ESP32'ye bağlama: çoğu ayarlı buck kutudan 12V'a yakın çıkar.
  • 18650 hücreleri BMS'siz kullanma; derin deşarj ve kısa devre koruması şart.
  • Teker plakaya sürtüyorsa iz genişliğini artır; motorlar çakışıyorsa mekanik sayfası uyarır.
Daha derin
  • Tekerleği TPU ile bas: O-ring'e gerek kalmaz, tutunma artar.
  • build123d sürümünü bir GitHub Action'a bağla: parametre değişince STL'ler otomatik üretilsin.
3.2

İki motor, diferansiyel sürüş kinematiği

ESP32 /cmd_vel'deki (v, ω)'yı iki tekerleğin hızına çeviriyor ve iki ayrı PID ile tutuyor. Teleop ile robotu odada sürebiliyorsun.

Önce simülasyonda
Dene:
  • "Klavyeyle sür"ü aç, robotu gezdir. Yeşil gerçek yol, mavi robotun sandığı.
  • Tüm hata kaydırıcılarını 0 yap: iki yol üst üste. Gerçek robotta hiçbir zaman böyle olmayacak.
Simülasyon yükleniyor…
Yap
Kod
include/pins.h
// include/pins.h — iki motor. I2C (21/22) IMU için boş bırakıldı.
#pragma once
// TB6612 A kanalı: sol motor
constexpr int PIN_PWMA = 25, PIN_AIN1 = 26, PIN_AIN2 = 27;
// TB6612 B kanalı: sağ motor (GPIO12 bir strapping pini, kullanma)
constexpr int PIN_PWMB = 32, PIN_BIN1 = 4, PIN_BIN2 = 16;
constexpr int PIN_STBY = 33;
// Encoder'lar
constexpr int PIN_ENC_L_A = 18, PIN_ENC_L_B = 19;
constexpr int PIN_ENC_R_A = 13, PIN_ENC_R_B = 14;
// I2C: IMU (4. hafta)
constexpr int PIN_SDA = 21, PIN_SCL = 22;
Kanıt: Teleop ile odada dolaşan robotun videosu.
Tuzaklar
  • Robot ileri giderken dönüyorsa bir motorun yönü ya da encoder'ı ters. Tekerleri havada tek tek test et.
  • İki motor aynı PWM'de farklı hızda döner; bu yüzden iki ayrı PID var. Açık döngüde düz gitmesini bekleme.
3.3

URDF ve TF ağacı

Robotun xacro tarifi RViz'de görünüyor; tekerlek eklemleri dönüyor, TF ağacı base_footprint → base_link → tekerlekler.

Yap
Kod
urdf/rover.urdf.xacro
<?xml version="1.0"?>
<!-- urdf/rover.urdf.xacro — ölçüler mekanik sayfasındaki şasiyle aynı olsun -->
<robot xmlns:xacro="http://www.ros.org/wiki/xacro" name="rover">
  <xacro:property name="wheel_r" value="0.0325"/>
  <xacro:property name="wheel_w" value="0.026"/>
  <xacro:property name="track" value="0.175"/>
  <xacro:property name="plate_l" value="0.17"/>
  <xacro:property name="plate_w" value="0.137"/>
  <xacro:property name="plate_z" value="0.045"/>

  <link name="base_footprint"/>
  <link name="base_link">
    <visual>
      <origin xyz="0 0 ${plate_z - wheel_r}"/>
      <geometry><box size="${plate_l} ${plate_w} 0.004"/></geometry>
      <material name="sari"><color rgba="0.96 0.77 0.26 1"/></material>
    </visual>
    <collision>
      <origin xyz="0 0 ${plate_z - wheel_r + 0.03}"/>
      <geometry><box size="${plate_l} ${plate_w} 0.07"/></geometry>
    </collision>
    <inertial>
      <mass value="0.75"/>
      <inertia ixx="0.002" iyy="0.003" izz="0.004" ixy="0" ixz="0" iyz="0"/>
    </inertial>
  </link>
  <!-- base_link teker aksının ortasında: diferansiyel sürüşte dönme merkezi orası -->
  <joint name="base_joint" type="fixed">
    <parent link="base_footprint"/><child link="base_link"/>
    <origin xyz="0 0 ${wheel_r}"/>
  </joint>

  <xacro:macro name="wheel" params="side y">
    <link name="${side}_wheel">
      <visual>
        <origin rpy="${pi/2} 0 0"/>
        <geometry><cylinder radius="${wheel_r}" length="${wheel_w}"/></geometry>
        <material name="koyu"><color rgba="0.15 0.15 0.15 1"/></material>
      </visual>
      <collision>
        <origin rpy="${pi/2} 0 0"/>
        <geometry><cylinder radius="${wheel_r}" length="${wheel_w}"/></geometry>
      </collision>
      <inertial>
        <mass value="0.03"/>
        <inertia ixx="1e-5" iyy="1.6e-5" izz="1e-5" ixy="0" ixz="0" iyz="0"/>
      </inertial>
    </link>
    <joint name="${side}_wheel_joint" type="continuous">
      <parent link="base_link"/><child link="${side}_wheel"/>
      <origin xyz="0 ${y} 0"/>
      <axis xyz="0 1 0"/>
    </joint>
  </xacro:macro>
  <xacro:wheel side="left" y="${track/2}"/>
  <xacro:wheel side="right" y="${-track/2}"/>

  <link name="caster">
    <visual><geometry><sphere radius="0.01"/></geometry></visual>
    <collision><geometry><sphere radius="0.01"/></geometry></collision>
    <inertial><mass value="0.01"/><inertia ixx="1e-7" iyy="1e-7" izz="1e-7" ixy="0" ixz="0" iyz="0"/></inertial>
  </link>
  <joint name="caster_joint" type="fixed">
    <parent link="base_link"/><child link="caster"/>
    <origin xyz="${-plate_l/2 + 0.018} 0 ${-wheel_r + 0.01}"/>
  </joint>

  <link name="imu_link"/>
  <joint name="imu_joint" type="fixed">
    <parent link="base_link"/><child link="imu_link"/>
    <origin xyz="0.035 0 ${plate_z - wheel_r + 0.01}"/>
  </joint>
</robot>
Kanıt: RViz ekran görüntüsü ve frames.pdf.
Tuzaklar
  • Silindirin ekseni z'dir; tekerlek için rpy="π/2 0 0" ile yatırılır. Unutursan teker yatay durur.
Daha derin
  • REP 105 ve REP 103'ü oku: map / odom / base_link anlaşması ve eksen yönleri (x ileri, y sol, z yukarı).
3.4

Odometry node'u (C++)

Tekerlek sayımlarından /odom ve odom→base_footprint TF yayınlayan bir C++ node'u. RViz'de robotu sürdükçe iz bırakıyor.

Önce simülasyonda
Dene:
  • Aks mesafesi hatasını +6% yap ve kareyi sür: kare açılıyor, çünkü her dönüş eksik.
  • Sol/sağ çap farkını 2% yap: düz giderken kıvrılıyor.
  • Patinajı 5% yap: encoder sayıyor ama robot gitmiyor; odometry fazlasını sanıyor.
  • Encoder'ı 40'a indir: kuantizasyon hatası da ekleniyor.
Simülasyon yükleniyor…
Yap
Kod
src/wheel_odometry.cpp
// src/wheel_odometry.cpp — tekerlek sayımlarından /odom ve odom→base_footprint TF
#include <cmath>
#include <geometry_msgs/msg/transform_stamped.hpp>
#include <nav_msgs/msg/odometry.hpp>
#include <rclcpp/rclcpp.hpp>
#include <std_msgs/msg/int32_multi_array.hpp>
#include <tf2/LinearMath/Quaternion.h>
#include <tf2_ros/transform_broadcaster.h>

class WheelOdometry : public rclcpp::Node {
 public:
  WheelOdometry() : Node("wheel_odometry") {
    r_ = declare_parameter("wheel_radius", 0.0325);
    r_left_scale_ = declare_parameter("left_radius_scale", 1.0);    // UMBmark'tan
    r_right_scale_ = declare_parameter("right_radius_scale", 1.0);
    track_ = declare_parameter("wheel_separation", 0.175);
    cpr_ = declare_parameter("ticks_per_rev", 1400.0);
    publish_tf_ = declare_parameter("publish_tf", true);  // EKF kullanınca false
    odom_frame_ = declare_parameter("odom_frame", std::string("odom"));
    base_frame_ = declare_parameter("base_frame", std::string("base_footprint"));

    pub_ = create_publisher<nav_msgs::msg::Odometry>("odom", 10);
    tf_ = std::make_unique<tf2_ros::TransformBroadcaster>(*this);
    sub_ = create_subscription<std_msgs::msg::Int32MultiArray>(
        "wheel_ticks", rclcpp::SensorDataQoS(),
        [this](const std_msgs::msg::Int32MultiArray& m) { onTicks(m); });
  }

 private:
  void onTicks(const std_msgs::msg::Int32MultiArray& m) {
    if (m.data.size() < 2) return;
    const rclcpp::Time stamp= now();
    const int32_t l= m.data[0], r= m.data[1];
    if (!init_) {
      lastL_= l; lastR_= r; last_= stamp; init_= true;
      return;
    }
    // int32 taşmasına dayanıklı fark
    const double dl= static_cast<int32_t>(l - lastL_) / cpr_ * 2 * M_PI * r_ * r_left_scale_;
    const double dr = static_cast<int32_t>(r - lastR_) / cpr_ * 2 * M_PI * r_ * r_right_scale_;
    lastL_ = l;
    lastR_ = r;
    const double dt = (stamp - last_).seconds();
    last_ = stamp;
    if (dt <= 0) return;

    const double ds= (dr + dl) / 2;
    const double dth= (dr - dl) / track_;
    // orta nokta integrasyonu: Euler'den daha az sistematik hata
    x_ = ds * std::cos(th_ + dth / 2);
    y_ = ds * std::sin(th_ + dth / 2);
    th_= std::atan2(std::sin(th_ + dth), std::cos(th_ + dth));

    tf2::Quaternion q;
    q.setRPY(0, 0, th_);
    nav_msgs::msg::Odometry o;
    o.header.stamp= stamp;
    o.header.frame_id= odom_frame_;
    o.child_frame_id= base_frame_;
    o.pose.pose.position.x= x_;
    o.pose.pose.position.y= y_;
    o.pose.pose.orientation.x= q.x();
    o.pose.pose.orientation.y= q.y();
    o.pose.pose.orientation.z= q.z();
    o.pose.pose.orientation.w= q.w();
    o.twist.twist.linear.x= ds / dt;
    o.twist.twist.angular.z= dth / dt;
    // kovaryans: EKF bunlara güvenir. Düz robotta z, roll, pitch'i "bilmiyorum" diye büyük ver.
    const double big= 1e6;
    for (int i : {0, 7}) o.pose.covariance[i]= 0.01;
    for (int i : {14, 21, 28}) o.pose.covariance[i]= big;
    o.pose.covariance[35]= 0.05;
    for (int i : {0}) o.twist.covariance[i]= 0.005;
    for (int i : {7, 14, 21, 28}) o.twist.covariance[i]= big;
    o.twist.covariance[35]= 0.03;
    pub_->publish(o);

    if (publish_tf_) {
      geometry_msgs::msg::TransformStamped t;
      t.header = o.header;
      t.child_frame_id = base_frame_;
      t.transform.translation.x = x_;
      t.transform.translation.y = y_;
      t.transform.rotation = o.pose.pose.orientation;
      tf_->sendTransform(t);
    }
  }

  double r_, r_left_scale_, r_right_scale_, track_, cpr_;
  bool publish_tf_;
  std::string odom_frame_, base_frame_;
  bool init_ = false;
  int32_t lastL_ = 0, lastR_ = 0;
  rclcpp::Time last_;
  double x_ = 0, y_ = 0, th_ = 0;
  rclcpp::Publisher<nav_msgs::msg::Odometry>::SharedPtr pub_;
  rclcpp::Subscription<std_msgs::msg::Int32MultiArray>::SharedPtr sub_;
  std::unique_ptr<tf2_ros::TransformBroadcaster> tf_;
};

int main(int argc, char** argv) {
  rclcpp::init(argc, argv);
  rclcpp::spin(std::make_shared<WheelOdometry>());
  rclcpp::shutdown();
  return 0;
}
Kanıt: 1 m düz sürüşte odom ile metre arasındaki fark (cm) ve RViz'de iz.
Tuzaklar
  • Sayaç int32 taşarsa fark işareti bozulur; farkı int32'ye cast ederek al (kodda var).
  • Zaman damgası olarak mesajın değil PC'nin saatini kullandık; micro-ROS saat senkronu kurarsan header stamp daha doğru olur.
Daha derin
  • Odometry'yi ESP32'de hesapla, yalnız sonucu yayınla. Avantajı ne, dezavantajı ne?
  • Runge-Kutta integrasyonunu ekle; 100 Hz'de Euler ile farkı gerçekten ölçülebilir mi?
3.5

UMBmark ile kalibrasyon

Kareyi iki yönde 5'er kez sürüp sistematik hataları ayırdın, düzeltme katsayılarını hesapladın. Kapanma hatası önce/sonra ölçülü.

Önce simülasyonda
Dene:
  • Kareyi saat yönü tersine sür, sonra saat yönüne: iki hata türü iki yönde farklı davranıyor.
  • UMBmark'ın fikri bu farktan iki bilinmeyeni (çap farkı ve aks) çözmek.
Simülasyon yükleniyor…
Yap
Kod
tools/umbmark.py
#!/usr/bin/env python3
"""umbmark.py — Borenstein & Feng (1996) UMBmark testi: düzeltme katsayıları.

Robotu 4 m'lik kare yerine kendi evine sığan L kenarla sür (örn. 1 m):
5 kez saat yönünde (CW), 5 kez tersine (CCW). Her turun sonunda başlangıç
noktasına göre (x, y) hatasını metreyle ölç ve aşağıya yaz. x = robotun ilk
gittiği yön, y = sol.

Formülleri makaleyle kontrol et: "Measurement and Correction of Systematic
Odometry Errors in Mobile Robots", IEEE T-RA 1996.
"""
import math

L = 1.0          # kare kenarı (m)
B = 0.175        # tekerlek merkezleri arası, kodda kullandığın değer (m)

cw = [(0.032, -0.041), (0.028, -0.036), (0.035, -0.044), (0.030, -0.039), (0.033, -0.040)]
ccw = [(-0.051, 0.012), (-0.048, 0.015), (-0.055, 0.010), (-0.049, 0.013), (-0.052, 0.011)]


def centroid(pts):
    return sum(p[0] for p in pts) / len(pts), sum(p[1] for p in pts) / len(pts)


xcw, ycw = centroid(cw)
xccw, yccw = centroid(ccw)

alpha = (xcw + xccw) / (-4 * L)          # rad — aks mesafesi hatasından
beta = (xcw - xccw) / (-4 * L)           # rad — tekerlek çapı farkından
R = (L / 2) / math.sin(beta / 2) if abs(beta) > 1e-9 else math.inf
Ed = (R + B / 2) / (R - B / 2) if math.isfinite(R) else 1.0
Eb = (math.pi / 2) / (math.pi / 2 - alpha)

left_scale = 2 / (Ed + 1)                # D_L = left_scale · D
right_scale = 2 / (1 / Ed + 1)           # D_R = right_scale · D
b_actual = Eb * B

print(f"alpha = {math.degrees(alpha):+.2f}°, beta = {math.degrees(beta):+.2f}°")
print(f"Ed (sağ/sol çap oranı) = {Ed:.4f}, Eb = {Eb:.4f}")
print("wheel_odometry parametreleri:")
print(f"  left_radius_scale:  {left_scale:.4f}")
print(f"  right_radius_scale: {right_scale:.4f}")
print(f"  wheel_separation:   {b_actual:.4f}")
Kanıt: Önce/sonra tablosu: CW ve CCW için ortalama kapanma hatası (cm).
Tuzaklar
  • Kareyi kapalı döngüyle (odom'a bakarak) sürersen test anlamsızlaşır: odometry kendi hatasını göremez.
  • Zemin değişirse (halı / parke) patinaj değişir; testi robotun gerçekten çalışacağı zeminde yap.
Daha derin
  • Rastgele hatayı (patinaj) kalibre edemezsin; onu 5. haftada IMU + EKF ile bastıracaksın.

Kavramlar

Diferansiyel sürüşv = (vR + vL) / 2, ω = (vR − vL) / b. Tüm iki tekerlekli robot yazılımının temeli.
URDF / xacroRobotun gövde ve eklemlerinin XML tarifi. RViz, Gazebo, Nav2, MoveIt hepsi bunu okur.
tf2Koordinat çerçeveleri ağacı: map → odom → base_footprint → base_link → sensörler.
OdometryTekerlek hareketini toplayarak konum tahmini. Hata birikir; tek başına uzun süre güvenilmez.
Sistematik vs rastgele hataYanlış ölçülmüş çap/aks sistematik (kalibre edilir); patinaj ve zemin rastgele (filtrelenir).
UMBmarkSaat yönünde ve tersinde kare sürerek iki sistematik hatayı (çap farkı, aks) ayırma yöntemi.

Kaynaklar