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 7 · 9–18 saat

Tıkla, gitsin

Nav2 ile otonom navigasyon + kendi web konsolun

Haritada bir noktaya tıklıyorsun, robot yolunu kendisi bulup gidiyor. Tıklamayı kendi yazdığın web konsolundan yapıyorsun: telefondan, evin dışından bile. Acil durdurma bir buton değil, sistemde en yüksek öncelikli kilit.

Günün sonunda elinde
  • Nav2 simde ve gerçek robotta; AMCL ile haritada konum
  • Costmap ve controller ayarları senin robotuna göre
  • twist_mux: e-stop > elle sürüş > Nav2
  • Next.js + roslib web konsolu: harita, canlı konum, tıkla-git, e-stop
  • Tailscale üzerinden telefondan yönetilen 1 dakikalık video
7.1

Costmap'i anla, sonra Nav2'yi simde kur

Nav2 Gazebo'daki robotunla, 6. haftanın haritasında çalışıyor. robot_radius ve inflation'ın planlanan yolu nasıl değiştirdiğini biliyorsun.

Önce simülasyonda
Dene:
  • Hedefi diğer odaya koy, robot yarıçapını 0.35 m'ye çıkar: 70 cm'lik kapı kapanıyor.
  • Maliyet ağırlığını 0 yap: yol duvarı sıyırıyor. 10 yap: ortadan geçiyor ama uzuyor.
  • A* ile Dijkstra'nın açtığı hücre sayısını karşılaştır.
Simülasyon yükleniyor…
Yap
Kod
config/nav2_params.yaml
# config/nav2_params.yaml — yalnız robota özgü kısımlar; gerisi nav2_bringup varsayılanları
# Tam dosya: /opt/ros/jazzy/share/nav2_bringup/params/nav2_params.yaml'ı kopyala, bunları değiştir.
amcl:
  ros__parameters:
    robot_model_type: "nav2_amcl::DifferentialMotionModel"
    base_frame_id: base_footprint
    scan_topic: scan
    alpha1: 0.2   # dönüşten dönüşe gürültü — odometry'n kötüyse artır
    alpha2: 0.2
    alpha3: 0.2
    alpha4: 0.2

controller_server:
  ros__parameters:
    controller_frequency: 20.0
    FollowPath:
      plugin: "nav2_regulated_pure_pursuit_controller::RegulatedPurePursuitController"
      desired_linear_vel: 0.25
      lookahead_dist: 0.4
      min_lookahead_dist: 0.2
      max_lookahead_dist: 0.7
      rotate_to_heading_angular_vel: 1.2
      use_rotate_to_heading: true          # diferansiyel robot yerinde dönebilir
      allow_reversing: false

local_costmap:
  local_costmap:
    ros__parameters:
      robot_base_frame: base_footprint
      rolling_window: true
      width: 3
      height: 3
      resolution: 0.05
      robot_radius: 0.12                   # şasinin köşegeninin yarısı + 1 cm
      inflation_layer:
        inflation_radius: 0.35
        cost_scaling_factor: 4.0

global_costmap:
  global_costmap:
    ros__parameters:
      robot_base_frame: base_footprint
      robot_radius: 0.12
      resolution: 0.05
      inflation_layer:
        inflation_radius: 0.35
        cost_scaling_factor: 4.0

planner_server:
  ros__parameters:
    GridBased:
      plugin: "nav2_navfn_planner::NavfnPlanner"
      use_astar: true
      allow_unknown: false

velocity_smoother:
  ros__parameters:
    max_velocity: [0.3, 0.0, 1.5]
    min_velocity: [-0.15, 0.0, -1.5]
    max_accel: [0.8, 0.0, 3.0]
    max_decel: [-1.0, 0.0, -3.0]
Kanıt: Simde iki oda arasında otonom gidişin ekran kaydı.
Tuzaklar
  • Robot hedefe gitmiyor, yerinde dönüyorsa: AMCL konumu yanlış ya da TF ağacında map → odom yok.
  • Nav2 "no valid path" diyorsa başlangıç ya da hedef inflation içinde kalıyor olabilir.
Daha derin
7.2

Gerçek robotta Nav2 ve e-stop

Gerçek robot evde bir odadan ötekine gidiyor. Her şeyden önce gelen bir acil durdurma kilidi var.

Yap
Kod
config/twist_mux.yaml
# config/twist_mux.yaml — e-stop her şeyin önünde
# Nav2 → /cmd_vel_nav, teleop → /cmd_vel_teleop, çıkış → /diff_drive_controller/cmd_vel
twist_mux:
  ros__parameters:
    topics:
      navigation:
        topic: cmd_vel_nav
        timeout: 0.5
        priority: 10
      teleop:
        topic: cmd_vel_teleop
        timeout: 0.5
        priority: 100        # elle sürüş Nav2'yi ezer
    locks:
      e_stop:
        topic: e_stop        # std_msgs/Bool: true olduğu sürece hiçbir hız geçmez
        timeout: 0.0         # 0 = zaman aşımı yok, kilit kalıcı
        priority: 255
Kanıt: Gerçek robotun iki oda arasındaki otonom gidişi ve e-stop testinin videosu.
Tuzaklar
  • WiFi gecikmesi controller'ı bozar: Nav2'yi Pi'de koşturmak gerekirse Pi 5 rahat yeter.
  • Robot dönerken harita kayıyorsa sorun neredeyse her zaman odometry ya da IMU yaw'ında.
7.3

Kendi web konsolun

Telefonundan açtığın bir web sayfası haritayı ve robotu canlı gösteriyor; tıkladığın yere robot gidiyor; büyük kırmızı buton her şeyi durduruyor.

Yap
Kod
app/robot/page.tsx
// app/robot/page.tsx — Next.js + roslib: harita, canlı konum, tıkla-git, e-stop
// Robotta: sudo apt install ros-jazzy-rosbridge-suite
//          ros2 launch rosbridge_server rosbridge_websocket_launch.xml   (ws://<pi>:9090)
// Uzaktan: Tailscale ile ws://rover:9090 (TLS gerekiyorsa tailscale serve)
'use client';

import { useEffect, useRef, useState } from 'react';
import ROSLIB from 'roslib';

type Grid = { w: number; h: number; res: number; ox: number; oy: number; data: number[] };

export default function RobotConsole() {
  const canvas = useRef<HTMLCanvasElement>(null);
  const ros = useRef<ROSLIB.Ros | null>(null);
  const goalTopic = useRef<ROSLIB.Topic | null>(null);
  const stopTopic = useRef<ROSLIB.Topic | null>(null);
  const [grid, setGrid] = useState<Grid | null>(null);
  const [pose, setPose] = useState<{ x: number; y: number; th: number } | null>(null);
  const [status, setStatus] = useState('bağlanıyor');
  const [stopped, setStopped] = useState(false);

  useEffect(() => {
    const r = new ROSLIB.Ros({ url: process.env.NEXT_PUBLIC_ROSBRIDGE ?? 'ws://rover:9090' });
    ros.current = r;
    r.on('connection', () => setStatus('bağlı'));
    r.on('close', () => setStatus('bağlantı yok'));
    r.on('error', () => setStatus('hata'));

    new ROSLIB.Topic({ ros: r, name: '/map', messageType: 'nav_msgs/msg/OccupancyGrid' })
      .subscribe((m: any) => setGrid({
        w: m.info.width, h: m.info.height, res: m.info.resolution,
        ox: m.info.origin.position.x, oy: m.info.origin.position.y, data: m.data,
      }));
    new ROSLIB.Topic({ ros: r, name: '/amcl_pose', messageType: 'geometry_msgs/msg/PoseWithCovarianceStamped' })
      .subscribe((m: any) => {
        const q = m.pose.pose.orientation;
        setPose({ x: m.pose.pose.position.x, y: m.pose.pose.position.y, th: Math.atan2(2 * (q.w * q.z), 1 - 2 * q.z * q.z) });
      });
    goalTopic.current = new ROSLIB.Topic({ ros: r, name: '/goal_pose', messageType: 'geometry_msgs/msg/PoseStamped' });
    stopTopic.current = new ROSLIB.Topic({ ros: r, name: '/e_stop', messageType: 'std_msgs/msg/Bool' });
    return () => r.close();
  }, []);

  // haritayı ve robotu çiz
  useEffect(() => {
    const c = canvas.current?.getContext('2d');
    if (!c || !grid) return;
    const img = c.createImageData(grid.w, grid.h);
    for (let y = 0; y < grid.h; y++)
      for (let x= 0; x < grid.w; x++) {
        const v= grid.data[y * grid.w + x];
        const px= v < 0 ? 205 : 255 - Math.round((v / 100) * 255);
        const o= ((grid.h - 1 - y) * grid.w + x) * 4;   // ROS haritası alttan yukarı
        img.data[o]= img.data[o + 1]= img.data[o + 2]= px;
        img.data[o + 3]= 255;
      }
    canvas.current!.width= grid.w;
    canvas.current!.height= grid.h;
    c.putImageData(img, 0, 0);
    if (pose) {
      const cx= (pose.x - grid.ox) / grid.res;
      const cy= grid.h - (pose.y - grid.oy) / grid.res;
      c.fillStyle= '#f5c542';
      c.beginPath();
      c.arc(cx, cy, 4, 0, Math.PI * 2);
      c.fill();
      c.strokeStyle= '#131c18';
      c.beginPath();
      c.moveTo(cx, cy);
      c.lineTo(cx + 8 * Math.cos(pose.th), cy - 8 * Math.sin(pose.th));
      c.stroke();
    }
  }, [grid, pose]);

  const onClick= (e: React.MouseEvent<HTMLCanvasElement>) => {
    if (!grid || stopped) return;
    const r = e.currentTarget.getBoundingClientRect();
    const gx = ((e.clientX - r.left) / r.width) * grid.w;
    const gy = ((e.clientY - r.top) / r.height) * grid.h;
    const x = grid.ox + gx * grid.res;
    const y = grid.oy + (grid.h - gy) * grid.res;
    goalTopic.current?.publish({ header: { frame_id: 'map' }, pose: { position: { x, y, z: 0 }, orientation: { x: 0, y: 0, z: 0, w: 1 } } });
  };

  const toggleStop = () => {
    const next = !stopped;
    setStopped(next);
    stopTopic.current?.publish({ data: next });
  };

  return (
    <main style={{ padding: 16, display: 'grid', gap: 12 }}>
      <div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
        <b>Rover</b> <span>{status}</span>
        <button onClick={toggleStop} style={{ marginLeft: 'auto', background: stopped ? '#1f7a4a' : '#c2382c', color: '#fff', padding: '12px 20px', fontWeight: 700 }}>
          {stopped ? 'Kilidi aç' : 'ACİL DUR'}
        </button>
      </div>
      <canvas ref={canvas} onClick={onClick} style={{ width: '100%', imageRendering: 'pixelated', cursor: 'crosshair' }} />
      <small>Haritaya tıkla: robot oraya gider. E-stop twist_mux kilidi: Nav2 çalışsa bile tekerleğe hız gitmez.</small>
    </main>
  );
}
Kanıt: Telefondan robotun yönetildiği 1 dakikalık ekran + oda videosu.
Tuzaklar
  • HTTPS sayfa ws:// adresine bağlanamaz; yerelde http ile aç ya da tailscale serve ile TLS ver.
  • rosbridge'e kimlik doğrulamasız açık bırakma; yalnız Tailscale ağı içinden erişilsin.
Daha derin
  • roslib yerine foxglove_bridge + @foxglove/ws-protocol: daha hızlı, ikili mesajlar.
  • Waypoint turu: odaları isimlendir (mutfak, salon), NavigateThroughPoses ile sırayla gez.
  • Konsolu Bun + Hono ile kendi küçük API'ne bağla; rosbridge'i tarayıcıya hiç açma.

Kavramlar

Nav2ROS 2'nin navigasyon yığını: global planner, local controller, costmap'ler, behavior tree.
AMCLBilinen haritada parçacık filtresiyle konum bulma. map → odom dönüşümünü yayınlar.
CostmapEngelleri robot yarıçapı kadar büyüten ve etrafına maliyet bandı çizen ızgara.
Behavior treeNav2'nin karar mantığı: planla, takip et, takılırsan geri çekil ve tekrar dene.
twist_muxBirden çok hız kaynağını önceliğe göre seçer; kilit (lock) ile hepsini durdurur.
rosbridgeROS 2 topic'lerini WebSocket üzerinden JSON olarak tarayıcıya açar.

Kaynaklar