Bu, konunun uzun ve teknik rehberi (ESP-IDF, MicroPython, rclcpp gibi alternatiflerle). Adım adım yol için Porti görevlerine dön.
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.
- 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
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.
- 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.
#!/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")- Kapıyı LLM'in prompt'una değil koda koy. "Hızlı gitme" demek bir güvenlik önlemi değildir.
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.
#!/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("> ")))Daha derin
- Robotu MCP sunucusu olarak aç: aynı araçları Claude Desktop ya da herhangi bir MCP istemcisi kullanabilsin.
Sesle konuş
Mikrofona Türkçe söylüyorsun, robot yapıyor ve sesli cevap veriyor. Her aşamanın gecikmesini biliyorsun.
#!/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")Daha derin
- Akışlı (streaming) STT ve TTS ile toplam gecikmeyi yarıya indir.