Bu, konunun uzun ve teknik rehberi (ESP-IDF, MicroPython, rclcpp gibi alternatiflerle). Adım adım yol için Porti görevlerine dön.
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.
- 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
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.
- 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.
# 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]- 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.
Gerçek robotta Nav2 ve e-stop
Gerçek robot evde bir odadan ötekine gidiyor. Her şeyden önce gelen bir acil durdurma kilidi var.
# 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
- 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.
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.
// 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>
);
}- 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.