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

Söyle, yapsın

LLM ajanı + deterministik safety gate + ses

Önceki sekiz haftanın üstüne bir beyin koyuyorsun. Sesli ya da yazılı komutu bir LLM ajanı araç çağrılarına çeviriyor: odaya git, dur, kolu çalıştır. Ajan ile motor arasında senin yazdığın, test edilmiş, deterministik bir kapı duruyor. Ajan önerir, kapı karar verir.

Günün sonunda elinde
  • Yalnız yüksek seviyeli araçlar: navigate_to, set_velocity, arm_skill, stop
  • Safety gate: hız limiti, geofence, taze durum, insan onayı, e-stop
  • Aynı ajan Strands Agents (Python) ve Vercel AI SDK (TypeScript) ile
  • Ses: STT → ajan → TTS; her aşamanın gecikmesi ölçülü
  • Kasıtlı tehlikeli komutların reddedildiği bir test videosu
9.1

Kapıyı ajandan önce yaz

Ajan yokken bile çalışan, birim testli bir safety gate node'u. Her araç çağrısı izin, kırpma, onay ya da ret ile dönüyor.

Önce simülasyonda
Dene:
  • E-stop'u bas: stop dışında her şey reddediliyor.
  • Heartbeat'i kapat: robot durumu bayatsa hiçbir hareket geçmiyor.
  • Ham eklem aracını açmayı dene ve neden önerilmediğini düşün.
Simülasyon yükleniyor…
Yap
Kod
safety_gate.py
#!/usr/bin/env python3
"""safety_gate.py — ajan ile robot arasındaki deterministik katman.

Ajan yalnız bu node'un servislerini çağırabilir. Her istek kurallardan geçer:
izin / kırp / insan onayı / red. Red her zaman okunabilir bir kodla döner,
ajan neden reddedildiğini görür ve planını değiştirir.
"""
import time
from dataclasses import dataclass

import rclpy
from geometry_msgs.msg import PoseStamped, Twist
from rclpy.node import Node
from std_msgs.msg import Bool

PLACES = {"mutfak": (4.2, 3.1, 0.0), "salon": (1.2, 1.0, 1.57)}
FENCE = (0.0, 0.0, 6.0, 5.0)        # xmin, ymin, xmax, ymax (harita çerçevesi)
V_MAX, W_MAX = 0.35, 1.2


@dataclass
class Verdict:
    ok: bool
    code: str
    detail: str


class SafetyGate(Node):
    def __init__(self):
        super().__init__("safety_gate")
        self.goal_pub = self.create_publisher(PoseStamped, "goal_pose", 10)
        self.vel_pub = self.create_publisher(Twist, "cmd_vel_teleop", 10)
        self.estop = False
        self.last_state = 0.0
        self.create_subscription(Bool, "e_stop", lambda m: setattr(self, "estop", m.data), 10)
        self.create_subscription(PoseStamped, "robot_pose", lambda _m: setattr(self, "last_state", time.time()), 10)

    def _common(self) -> Verdict | None:
        if self.estop:
            return Verdict(False, "E_STOP_LATCHED", "acil durdurma basılı; bir insan sıfırlamalı")
        if time.time() - self.last_state > 0.5:
            return Verdict(False, "STALE_STATE", "robot durumu 500 ms'den eski")
        return None

    def navigate_to(self, place: str) -> Verdict:
        if (v := self._common()):
            return v
        if place not in PLACES:
            return Verdict(False, "UNKNOWN_PLACE", "bilinen yerler: " + ", ".join(PLACES))
        x, y, yaw = PLACES[place]
        if not (FENCE[0] <= x <= FENCE[2] and FENCE[1] <= y <= FENCE[3]):
            return Verdict(False, "OUT_OF_GEOFENCE", f"{place} izinli alanın dışında")
        msg = PoseStamped()
        msg.header.frame_id = "map"
        msg.header.stamp = self.get_clock().now().to_msg()
        msg.pose.position.x, msg.pose.position.y = x, y
        import math
        msg.pose.orientation.z, msg.pose.orientation.w = math.sin(yaw / 2), math.cos(yaw / 2)
        self.goal_pub.publish(msg)
        return Verdict(True, "OK", f"{place} hedefi Nav2'ye gönderildi")

    def set_velocity(self, v: float, w: float) -> Verdict:
        if (bad := self._common()):
            return bad
        cv, cw = max(-V_MAX, min(V_MAX, v)), max(-W_MAX, min(W_MAX, w))
        t = Twist()
        t.linear.x, t.angular.z = cv, cw
        self.vel_pub.publish(t)
        clipped = (cv, cw) != (v, w)
        return Verdict(True, "CLAMPED" if clipped else "OK", f"v={cv:.2f} m/s, w={cw:.2f} rad/s")

    def stop(self) -> Verdict:
        self.vel_pub.publish(Twist())       # stop hiçbir koşulda reddedilmez
        return Verdict(True, "OK", "durdu")
Kanıt: Kapının test çıktısı: her kural için en az bir izin ve bir ret senaryosu.
Tuzaklar
  • Kapıyı LLM'in prompt'una değil koda koy. "Hızlı gitme" demek bir güvenlik önlemi değildir.
Daha derin
9.2

Ajanı bağla: iki stack

"Mutfağa git, sonra salona dön" yazınca robot iki hedefe sırayla gidiyor. "Balkona çık" deyince ajan reddi açıklıyor.

Yap
Kod
agent.py
#!/usr/bin/env python3
"""agent.py — Strands Agents ile robot ajanı. Araçlar yalnız safety gate'i çağırır.

  pip install strands-agents
"""
import rclpy
from strands import Agent, tool

from safety_gate import SafetyGate

rclpy.init()
gate = SafetyGate()


def spin():
    rclpy.spin_once(gate, timeout_sec=0.05)


@tool
def navigate_to(place: str) -> str:
    """Robotu haritadaki isimli bir yere gönderir. Bilinen yerler: mutfak, salon."""
    spin()
    v = gate.navigate_to(place)
    return f"{v.code}: {v.detail}"


@tool
def set_velocity(v: float, w: float) -> str:
    """Kısa bir an için hız verir. v: m/s (ileri +), w: rad/s (sola +). Limitler kapıda kırpılır."""
    spin()
    r = gate.set_velocity(v, w)
    return f"{r.code}: {r.detail}"


@tool
def stop() -> str:
    """Robotu hemen durdurur."""
    return gate.stop().detail


agent = Agent(
    system_prompt=(
        "Sen evdeki bir mobil robotun ajanısın. Yalnız verilen araçları kullan. "
        "Bir araç RED kodu dönerse nedenini kullanıcıya açıkla, aynı isteği zorlamaya çalışma."
    ),
    tools=[navigate_to, set_velocity, stop],
)

if __name__ == "__main__":
    while True:
        print(agent(input("> ")))
Kanıt: Üç komutun (biri reddedilen) ekran + oda videosu.
Daha derin
  • Robotu MCP sunucusu olarak aç: aynı araçları Claude Desktop ya da herhangi bir MCP istemcisi kullanabilsin.
9.3

Sesle konuş

Mikrofona Türkçe söylüyorsun, robot yapıyor ve sesli cevap veriyor. Her aşamanın gecikmesini biliyorsun.

Yap
Kod
voice.py
#!/usr/bin/env python3
"""voice.py — ses → metin → ajan → ses. Gecikme bütçesini ölç.

  pip install faster-whisper sounddevice numpy
  Piper TTS: Türkçe ses modelini indir, piper komutunu PATH'e koy.
"""
import subprocess
import time

import numpy as np
import sounddevice as sd
from faster_whisper import WhisperModel

from agent import agent

stt = WhisperModel("small", device="cpu", compute_type="int8")
SR = 16000

while True:
    input("Enter'a bas, 4 saniye konuş…")
    audio = sd.rec(int(4 * SR), samplerate=SR, channels=1, dtype="float32")
    sd.wait()
    t0 = time.time()
    segments, _ = stt.transcribe(audio[:, 0], language="tr", vad_filter=True)
    text = " ".join(s.text for s in segments).strip()
    t1 = time.time()
    reply = str(agent(text))
    t2 = time.time()
    subprocess.run(["piper", "--model", "tr_TR-model.onnx", "--output_file", "/tmp/r.wav"], input=reply.encode(), check=True)
    subprocess.run(["aplay", "/tmp/r.wav"], check=False)
    t3 = time.time()
    print(f"'{text}' → '{reply}'  |  STT {t1-t0:.2f}s · ajan {t2-t1:.2f}s · TTS {t3-t2:.2f}s")
Kanıt: "Mutfağa git" dediğin, robotun gittiği ve cevap verdiği video; altında gecikme tablosu.
Daha derin
  • Akışlı (streaming) STT ve TTS ile toplam gecikmeyi yarıya indir.

Kavramlar

Tool callingLLM'in fonksiyon çağırması. Bu sefer fonksiyonların ucunda fiziksel bir robot var.
Safety gateAjan isteği ile motor arasındaki deterministik, test edilmiş katman. Prompt güvenlik değildir.
Ret koduKapının okunabilir cevabı (OUT_OF_GEOFENCE gibi). Ajan bunu görür ve planını değiştirir.
İnsan onayıBazı aksiyonlar (kol hareketi) ancak bir insan onaylayınca çalışır.
Gecikme bütçesiSTT + LLM + TTS + robot: konuşmanın doğal hissettirmesi için her biri ölçülmeli.

Kaynaklar