Yazılım
Yaşam Döngüsü
Bir maçta iki faz var. Önce otonom: joystick yok, robot yalnızca koda göre hareket eder; varsayılan süre 30 saniye, arayüzden ayarlanabilir. Otonom süresi dolunca robot durur ve teleop otomatik seçilir ama başlamaz; sürücü Init ile Start yapınca teleop başlar: kumandayla kontrol edilir, maç sonuna kadar devam eder.
Bu iki fazın dışında robotun iki durumu daha var. Maç başlamadan önce Driver Station'dan mod seçilir (Otonom / TeleOp), sonra Init yapılır: robot hazır hale gelir ama hareket etmez. Maç bitince Stop ile robot durur; acil durum için ayrı bir Emergency Stop var (aşağıda). Bu akış — mod seç, Init, Start, Stop — FTC'deki OpMode modelinin aynısıdır.
Kodda maçın fazlarına karşılık gelen hook'lar var. Normal Arduino'da setup() ve loop() yazılır; Probot'ta bunların yerine aşağıdaki fonksiyonlar tanımlanır:
void autonomousInit() {} // Otonom: Init'te 1 kez
void autonomousLoop() { delay(20); } // Otonom: Start sonrası ~50 Hz (ZORUNLU)
void autonomousStop() {} // Otonomdan her çıkışta 1 kez (ZORUNLU)
void teleopInit() {} // TeleOp: Init'te 1 kez
void teleopLoop() { delay(20); } // TeleOp: Start sonrası ~50 Hz (ZORUNLU)
void teleopStop() {} // TeleOp'tan her çıkışta 1 kez (ZORUNLU)
Bu fonksiyonlardan dördü zorunludur (autonomousLoop, autonomousStop, teleopLoop, teleopStop); kalanlar opsiyoneldir, tanımlanmazsa boş sayılır. Zorunlu bir hook tanımsızsa bağlama (link) hatası alınır. setup() ve loop() tanımlanmaz; kütüphane sahip, tanımlanırsa derleme hatası verir.
Faz akışı:
mod seç (Otonom | TeleOp)
│
STOPPED ──Init──> INIT[mod] ──Start──> RUN[mod] ──Stop──> STOPPED
(modInit 1 kez, (modStart 1 kez,
sonra modInitLoop) sonra modLoop ~50 Hz)
RUN[Otonom] ─süre dolunca─> autonomousStop() → TRANSITION
(TeleOp otomatik seçilir ama BAŞLAMAZ; sürücü Init + Start yapar)
Otonom süresi arayüzden ayarlanır ve yalnızca otonom fazını sınırlar. Süre dolunca robot durur; teleop otomatik seçilir ama sürücü Init ile Start yapana kadar başlamaz. Otonomsuz bir maç için doğrudan teleop modu seçilir.
Tüm hook'lar tek kalıcı task üzerinde çalışır; task boot'ta açılır ve normal işleyişte asla öldürülmez (static/global değişkenler bu yüzden fazlar arasında yaşar). Faz geçişleri kooperatiftir ve her zaman loop turu sınırında yapılır — Stop ya da faz değişimi kodu iş ortasında kesemez.
Loop sözleşmesi: Her tur bir gün mutlaka dönmeli. Bloke eden çağrı serbest, sonsuz bloke yasak — I2C/sensör çağrılarına timeout koy (Wire.setTimeOut(50) gibi). Bir tur 2 saniyeden uzun sürerse halt-safe devreye girer: joystick sıfırlanır, LED kırmızı yanıp söner. Task öldürülmez; tur bitince temizlenir. Bkz. Hatalar - PB-E301 Deadline Miss.
Stop kooperatiftir: o anki tur bittikten sonra aktif modun stop hook'u (autonomousStop() ya da teleopStop()) çalışır. Anında kesme için arayüzdeki Emergency Stop (kısayolu Space — arayüz açıkken her zaman çalışır).
Emergency Stop terminaldir ve donmuş bir loop'u bile durdurur: kullanıcı task'ı öldürülür, aktif modun stop hook'u taze bir task'ta watchdog'lu çalıştırılır (PROBOT_ESTOP_END_MS, 500 ms; aşılırsa çip reboot eder), varsa PROBOT_ESTOP_ENABLE_PIN LOW'a çekilir ve robot reboot'a kadar kilitlenir — Init/Start reddedilir, kilidi arayüzdeki reboot ya da güç döngüsü açar.
Koddan tetiklemek için probot::emergencyStop() çağrılır. Her task'tan — kullanıcı hook'ları dahil — güvenlidir: sadece bir bayrak set eder, gerçek sırayı kütüphane yürütür.
Yazılım E-stop'u donanım E-stop'un yerini tutmaz
Enable pini yazılım kontrolündedir; çip tamamen kilitlenirse çalışmayabilir. Gerçek güvenlik garantisi, güç hattına konan bağımsız fiziksel E-stop'tur.
İleri Seviye: initLoop ve start
Zorunlu init/loop/stop üçlüsünün yanında, her mod için iki opsiyonel hook daha var; çoğu robot bunlara ihtiyaç duymaz.
autonomousInitLoop() / teleopInitLoop(): Init ile Start arasında sürekli çağrılır. Robot kımıldamadan (girişler nötr, RSL sabit) kod koşturmak için — kamerayla saha randomizasyonu okuma, gyro/sensör kalibrasyon durumunu telemetriye yayınlama, maç öncesi kontrol. Deadline (stall) denetimi bu evrede de işler.
autonomousStart() / teleopStart(): Start anında bir kezlik iş (zaman damgası, state reset) için. Çoğu durumda loop'un ilk turu yeterli olduğundan nadiren gerekir.
0.4.0 notu
robotInit() ve robotEnd() 0.4.0'da kaldırıldı. Kurulum kodunu mod init'lerine taşı (tekrarına dayanıklı işler için ortak bir setupHardware() fonksiyonu yazıp iki init'ten çağır), durdurma kodunu iki stop hook'una taşı (ortak stopMotors()). Eski bir sketch derlenirse autonomousStop/teleopStop tanımlı olmadığı için bağlama (link) hatası alınır; robotInit/robotEnd tanımlı kalırsa hata vermez ama artık asla çağrılmaz. Otonom aç/kapa ayarı da kalktı: otonomsuz başlamak için teleop modunu seç.
Joystick API
Kumanda, USB veya Bluetooth üzerinden tabletteki tarayıcıya bağlanır. Tarayıcı kumandanın çubuk ve buton değerlerini sürekli alır, Driver Station arayüzü bu değerleri WebSocket bağlantısı üzerinden ESP32'ye gönderir. probot::io::joystick_api bu veriye ESP32 tarafından erişmek için kullanılan arayüz.
Her loop turunda aşağıdaki satırla joystick nesnesi alınır:
auto js = probot::io::joystick_api::makeDefault();
Sonra js.getLeftY(), js.getA() gibi metodlarla değerler okunur. Bu nesneyi her tur yeniden almak normaldir; arka planda herhangi bir bağlantı işlemi yapmaz, sadece en güncel değerleri getirir.
Eksenler
Kumandadaki analog çubuklar eksen değeri döner. Değer -1 ile +1 arasında bir ondalık sayıdır: tam ileri +1, tam geri -1, ortada 0.
Sol çubuğu ileri itince getLeftY() pozitif değer verir, geri çekince negatif. Sağa yatırınca getLeftX() pozitif olur. Sağ çubuk da aynı mantıkla çalışır.
| Metod | Açıklama |
|---|---|
getLeftX() |
Sol çubuk yatay. Sağ = pozitif |
getLeftY() |
Sol çubuk dikey. İleri = pozitif |
getRightX() |
Sağ çubuk yatay. Sağ = pozitif |
getRightY() |
Sağ çubuk dikey. İleri = pozitif |
getLeftTriggerAxis() |
Sol tetik. Basılı = 1.0, bırakılmış = 0.0 |
getRightTriggerAxis() |
Sağ tetik. Basılı = 1.0, bırakılmış = 0.0 |
Deadzone nedir? Fiziksel kumandalar mekanik olarak tam sıfıra dönmez. Çubuğu bıraksan bile sensör 0.02, 0.03 gibi küçük bir değer okuyabilir. Bu değer motor koduna direkt beslenirse motor çok yavaş da olsa döner. Deadzone bu sorunu çözer: belirli bir eşiğin altındaki değerleri sıfır kabul eder. Varsayılan eşik 0.08; çubuğu bıraktığında -0.08 ile +0.08 arasında kalan her değer sıfır döner.
Butonlar
Buton metodları bool döner: basılıysa true, bırakılmışsa false.
Buton isimleri Xbox standardını esas alır. PlayStation kumandası kullanılıyorsa karşılık gelen isimler de çalışır; aynı fiziksel butona karşılık gelir.
| Xbox | PlayStation | Metod |
|---|---|---|
| A | Cross (×) | getA() / getCross() |
| B | Circle (○) | getB() / getCircle() |
| X | Square (□) | getX() / getSquare() |
| Y | Triangle (△) | getY() / getTriangle() |
| LB | L1 | getLB() |
| RB | R1 | getRB() |
| Back / View | Select | getBack() |
| Start / Menu | Options | getStart() |
| L3 (sol çubuğa bas) | L3 | getLeftStickButton() |
| R3 (sağ çubuğa bas) | R3 | getRightStickButton() |
D-Pad (yön tuşları) iki şekilde okunabilir:
// Her yön ayrı ayrı bool olarak:
js.getDpadUp();
js.getDpadRight();
js.getDpadDown();
js.getDpadLeft();
// Tek değer olarak açı (derece):
js.getPOV(); // -1 = hiçbiri, 0 = yukarı, 90 = sağ, 180 = aşağı, 270 = sol
Deadzone Ayarı
Varsayılan 0.08 çoğu kumanda için yeterlidir. Çubuk bırakıldığında motor hâlâ yavaşça dönüyorsa deadzone değeri artırılabilir:
probot::io::joystick_api::Options opt;
opt.deadzone = 0.12f;
auto js = probot::io::joystick_api::makeDefault(opt);
Kumanda Profili
Farklı kumanda modelleri buton ve eksen numaralarını farklı sıralar. Logitech F310'da A butonu tarayıcıya "buton 0" olarak gelirken başka bir kumandada farklı gelebilir. Kütüphane bu farkı profil sistemiyle çözer: hangi numara hangi butona karşılık geliyor sorusunun cevabı profilde tanımlı.
Varsayılan profil logitech-f310; Logitech F310 ve yarışmalarda sık kullanılan kumandaların büyük çoğunluğu için çalışır. Xbox One ve DualShock 4 gibi modern kumandalar genellikle standard profiliyle çalışır.
Profil değiştirmek için teleopInit() içinde:
void teleopInit() {
probot::io::joystick_mapping::setActiveByName("standard");
}
| Profil | Kumanda |
|---|---|
"logitech-f310" / "f310" |
Logitech F310 (varsayılan) |
"standard" / "xbox" / "ds4" / "ps" |
Xbox One, DualShock 4 ve W3C standardına uyan diğerleri |
"axis9-dpad" |
D-Pad'i eksen olarak değil 9. eksen üzerinden gönderen bazı kumandalar |
Kumanda düğmelere basıldığında hiç tanınmıyorsa ya da yanlış buton tetikleniyorsa profil uyumsuzluğu olabilir.
Failsafe
Joystick verisi 500 ms kesilirse tüm eksen ve buton değerleri otomatik olarak sıfır okunur. Motor hızını doğrudan eksen değerine bağlamak güvenlidir; bağlantı kopunca eksenler sıfıra döner ve motor durur.
Telemetri
Telemetri, kodun içinden Driver Station ekranına metin göndermek için kullanılır. Robot çalışırken değişkenleri, sensör okumalarını veya durum bilgilerini ekranda canlı görmek mümkün. Debug için Serial monitörüne yazmak yerine telemetri kullanmak sahada çok daha pratik; bilgisayar bağlı olmak zorunda değil.
probot::print("merhaba");
probot::println("satır sonu ekler");
probot::printf("speed=%.2f load=%d\n", speed, load);
probot::clearTelemetry();
Arka planda 256 baytlık bir tampon var. Her loop turunda önce clearTelemetry() çağır, sonra yazmak istediğin değerleri yaz. Yoksa önceki turdan kalan değerler ekranda birikir ve okuması güçleşir.
Motor Kontrolü
Motor sürücüler (H-köprü), ESP32'nin GPIO pinlerini robot motorlarını döndürmeye yetecek akıma dönüştürür. BTS7960, yarışmalarda sık kullanılan ve yüksek akım taşıyabilen bir sürücü. Kütüphanede hazır motor sürücü sınıfı yok; doğrudan analogWrite kullanılır.
BTS7960 iki PWM pini üzerinden çalışır: RPWM motoru bir yöne, LPWM diğer yöne sürer. İkisine aynı anda sinyal verilmez; sürücü hasar görür. Hız analogWrite ile 0-255 arası bir PWM değeriyle ayarlanır, yön ise hangi pine sinyal verildiğiyle belirlenir:
const int LEFT_RPWM = 16;
const int LEFT_LPWM = 17;
void motorLeft(float speed) {
speed = constrain(speed, -1.0f, 1.0f);
if (speed >= 0) {
analogWrite(LEFT_RPWM, (int)(speed * 255));
analogWrite(LEFT_LPWM, 0);
} else {
analogWrite(LEFT_RPWM, 0);
analogWrite(LEFT_LPWM, (int)(-speed * 255));
}
}
void teleopInit() {
pinMode(LEFT_RPWM, OUTPUT);
pinMode(LEFT_LPWM, OUTPUT);
}
void stopMotors() {
analogWrite(LEFT_RPWM, 0);
analogWrite(LEFT_LPWM, 0);
}
void teleopStop() { stopMotors(); }
void autonomousStop() { stopMotors(); }
void teleopLoop() {
auto js = probot::io::joystick_api::makeDefault();
motorLeft(js.getLeftY());
delay(20);
}
Joystick ekseni -1..+1 arası döner; fonksiyon bunu yön ve hıza dönüştürür. Joystick bağlantısı kopunca eksen sıfıra döner ve motor durur.
Motorları durdurma kodunu her iki stop hook'una da yaz; yukarıdaki gibi ortak bir stopMotors() fonksiyonu yazıp autonomousStop() ile teleopStop()'tan çağırmak en temizi. Stop yapılınca loop bir daha çağrılmaz; durdurma kodu stop hook'larında yoksa motorlar dönmeye devam eder.
Sağ motor için aynı fonksiyonu farklı pinlerle tekrarla (motorRight) ve teleopLoop()'ta motorRight(js.getRightY()) olarak çağır.
Şasi Tipleri
Şasi, birden fazla motorun birlikte robotu hareket ettirmesi. Aynı fiziksel taban farklı kumanda eşlemeleriyle sürülebilir; tekerlek tipi de robotun neler yapabileceğini belirler. Örnek Robot sayfasında bu şasiler adım adım class hâline getiriliyor; burada hangi durumda hangisinin seçileceği ve kumanda eşlemeleri ele alınıyor.
Tank (diferansiyel). İki taraf bağımsız sürülür. En basit ve en sağlam şasi. İki kumanda tarzı yaygın:
// Tank kontrolü: her çubuk bir tarafı sürer
motorLeft(js.getLeftY());
motorRight(js.getRightY());
// Arcade kontrolü: bir çubuk ileri/geri, diğeri dönüş
float fwd = js.getLeftY();
float turn = js.getRightX();
motorLeft(fwd + turn);
motorRight(fwd - turn);
Tank kontrolünde sürücü her tarafı ayrı düşünür; arcade çoğu sürücü için daha sezgisel. Aynı donanım, sadece eşleme farklı.
Mecanum. Dört mecanum tekerlek; robot ileri/geri ve dönüşün yanında yana da kayabilir (strafe). Daha çevik ama mekanik olarak karmaşık ve kaymaya açık. Üç eksen birden sürülür; tekerlek hız formülleri ve normalizasyon Örnek Robot - Mecanum Şasi sayfasında.
Hangisi seçilmeli? Çoğu yarışma görevi için tank yeter ve en az sorun çıkarır. Yana kayma gerçekten gerekiyorsa (dar alanda hizalanma gibi) mecanum düşünülür; aksi halde ek karmaşıklık genellikle değmez.
Sürüşü İyileştirmek
Çubuk değerini doğrudan motora vermek çalışır ama sürüş kaba olur. Birkaç basit teknik kontrolü belirgin biçimde iyileştirir.
Üs alma. Çubuğun küçük hareketleri robotu sert tepki verdirir; hassas manevra zorlaşır. Değerin küpünü almak, işareti koruyarak düşük bölgeyi yumuşatır: çubuk yarıdayken çıkış 0.5³ = 0.125, yani çok daha yavaş; sona doğru hızla artar. Düşük hızda hassas, yüksek hızda tam güç.
float shape(float x) { return x * x * x; } // küp; işaret korunur
motorLeft(shape(js.getLeftY()));
Daha hafif bir seçenek değeri mutlak değeriyle çarpmak (x * fabs(x)): yine işaret korunur, etki küpten daha az olur.
Slew rate. Çubuk aniden tam ileri itilince motora bir anda tam güç gider; bu sıçrama tekerleği patinaj yaptırır veya robotu sarsar. Slew rate, çıkışın bir turdan diğerine ne kadar değişebileceğini sınırlar; komut yumuşar.
float slew(float target, float current, float max_step) {
float diff = constrain(target - current, -max_step, max_step);
return current + diff;
}
static float drive_out = 0.0f;
drive_out = slew(js.getLeftY(), drive_out, 0.05f); // tur başına en fazla 0.05
motorLeft(drive_out);
max_step küçüldükçe yumuşama artar ama tepki gecikir; deneyerek dengelenir.
Deadzone. Çubuk merkezde tam sıfıra dönmez; küçük artık değerler motoru hafifçe döndürür. Kütüphane bunu varsayılan 0.08 eşikle zaten temizliyor, ayarı Joystick API bölümünde.
Servo
Hobi servoları 50 Hz PWM sinyali bekler: her 20 ms'de bir darbe gelir, darbe genişliği açıyı belirler. 1000 µs ≈ 0°, 1500 µs ≈ 90°, 2000 µs ≈ 180°. Gerçek uçlar servoya göre değişir; kalibrasyon gerekebilir.
0.3.0 notu
Eski sürümlerdeki probot::devices::Servo sınıfı 0.3.0'da kaldırıldı; kütüphane servo sınıfı sağlamaz. Servo aşağıdaki ham LEDC kalıbıyla sürülür.
Bu sinyali üretmek için ESP32'nin LEDC donanımı kullanılır. analogWrite (motorlar) yaklaşık 1 kHz çalışır, servo için çok hızlıdır. Servo için LEDC'yi ayrıca 50 Hz'e kurmak gerekir.
const uint8_t SERVO_PIN = 4;
void servoAngle(uint8_t pin, float deg, uint16_t minUs = 500, uint16_t maxUs = 2500) {
deg = constrain(deg, 0.0f, 180.0f);
uint16_t us = minUs + (uint16_t)(deg / 180.0f * (maxUs - minUs));
ledcWrite(pin, (uint32_t)us * 16383 / 20000);
}
void teleopInit() {
// analogWrite (motorlar) düşük LEDC kanallarını kullanır (0, 1, 2…).
// Kanal 7 seçmek timer çakışmasını önler.
ledcAttachChannel(SERVO_PIN, 50, 14, 7);
}
void teleopLoop() {
auto js = probot::io::joystick_api::makeDefault();
if (js.getA()) servoAngle(SERVO_PIN, 0.0f);
else if (js.getB()) servoAngle(SERVO_PIN, 180.0f);
delay(20);
}
ledcAttachChannel(pin, frekans, çözünürlük_bit, kanal): 50 Hz frekans, 14 bit çözünürlük. 14 bit → 16383 = tam 20 ms'lik period. us * 16383 / 20000 dönüşümü µs'yi duty değerine çevirir.
Birden fazla servo için her birine ayrı pin ve yüksek kanal ver (7, 6, 5… gibi üstten aşağı); motorlar analogWrite ile kanalları alttan (0, 1, 2…) aldığından timer çakışması olmaz. Hepsi 50 Hz olduğundan servolar aynı timer'ı paylaşabilir.
teleopInit()'te ledcAttachChannel çağrıldıktan sonra ilk servoAngle() çağrısına kadar servo sinyal bekler; robot açılışta servo aniden zıplamaz.
Güç: Servoyu ESP32 kartının 5V/3.3V pininden besleme. Bu pinler yüksek akımı karşılamaz; servo seğirir veya ESP32 sıfırlanır. Ayrı 5-6V kaynak (BEC/UBEC) kullan. Sinyal kablosu ESP32 pinine, güç ve GND BEC'e bağlanır; BEC GND ile ESP32 GND ortak tutulmalı.
Subsystem Deseni
Robot büyüdükçe tüm mantığı teleopLoop içine yığmak yönetilemez hâle gelir. Önerilen yapı: her mekanizmayı (şasi, kol, slider, gripper) ayrı bir class olarak yazmak. Her subsystem aynı arayüzü paylaşır:
init(): pinleri ayarlar; her modun init'inde (teleopInit()/autonomousInit()) çağrılır.- Eylem metodları:
up(),set(),open()gibi dışarıdan verilen komutlar. stop(): mekanizmayı güvenli hâle getirir; her iki stop hook'unda (teleopStop()/autonomousStop()) çağrılır.- Gerekiyorsa
periodic(): her loop turunda çağrılan, sensör okuyan veya hedefe yaklaşan iç mantık.
Bu desende teleopLoop yalnızca girişleri subsystem metodlarına bağlar; donanım detayı subsystem'in içinde kalır:
void teleopLoop() {
auto js = probot::io::joystick_api::makeDefault();
drive.arcade(js.getLeftY(), js.getRightX());
if (js.getRB()) intake.run(); else intake.stop();
slider.periodic(); // sensör tabanlı subsystem'ler her tur güncellenir
delay(20);
}
Bir mekanizmayı değiştirmek diğerlerini etkilemez ve otonom da aynı metodları çağırır. Örnek Robot sayfasındaki tüm sistemler bu desenle yazılı.
Subsystem Örnekleri: Slider
Slider (lineer hareket eden kızak), yarışmalarda en sık kullanılan mekanizmalardan. Aynı slider üç farklı şekilde sürülebilir; karmaşıklık ve hassasiyet arttıkça farklı yöntemler gerekir. Sırayla en basitten gelişmişe.
1. Limit Switch ve Sabit Güç
En basit yaklaşım: butona basıldıkça sabit güçle hareket et, uçtaki limit switch tetiklenince dur. Konum bilgisi yok; sadece "yukarı git" ve "aşağı git" var. Çoğu görev için bu yeterli.
class slider_t {
public:
slider_t(int rpwm, int lpwm, int limit_low, int limit_high)
: _rpwm(rpwm), _lpwm(lpwm), _limit_low(limit_low), _limit_high(limit_high) {}
void init() {
pinMode(_rpwm, OUTPUT); pinMode(_lpwm, OUTPUT);
pinMode(_limit_low, INPUT_PULLUP); pinMode(_limit_high, INPUT_PULLUP);
}
void up() { if (digitalRead(_limit_high) == LOW) { stop(); return; } _drive(0.8f); }
void down() { if (digitalRead(_limit_low) == LOW) { stop(); return; } _drive(-0.8f); }
void stop() { _drive(0.0f); }
private:
void _drive(float speed) {
if (speed >= 0) { analogWrite(_rpwm, (int)(speed * 255)); analogWrite(_lpwm, 0); }
else { analogWrite(_rpwm, 0); analogWrite(_lpwm, (int)(-speed * 255)); }
}
int _rpwm, _lpwm, _limit_low, _limit_high;
};
slider_t slider(20, 21, 22, 23);
Kullanımı Örnek Robot - Kol ile aynı mantık: limit switch INPUT_PULLUP ile okunur, basılı = LOW = o yönde dur.
2. Limit Switch Yoksa: Süreyle
Limit switch takılı değilse konum süreyle tahmin edilir: bir komut sliderı sabit süre boyunca sürer. periodic() her turda süreyi kontrol eder ve dolduğunda durur; loop bloke edilmez.
class timed_slider_t {
public:
timed_slider_t(int rpwm, int lpwm) : _rpwm(rpwm), _lpwm(lpwm) {}
void init() { pinMode(_rpwm, OUTPUT); pinMode(_lpwm, OUTPUT); }
void raise() { _start( 0.8f, 1200); } // 1.2 sn yukarı
void lower() { _start(-0.8f, 1200); } // 1.2 sn aşağı
void periodic() {
if (_running && millis() - _t0 >= _dur) stop();
}
void stop() { _running = false; _drive(0.0f); }
private:
void _start(float speed, uint32_t dur) {
_t0 = millis(); _dur = dur; _running = true; _drive(speed);
}
void _drive(float speed) {
if (speed >= 0) { analogWrite(_rpwm, (int)(speed * 255)); analogWrite(_lpwm, 0); }
else { analogWrite(_rpwm, 0); analogWrite(_lpwm, (int)(-speed * 255)); }
}
int _rpwm, _lpwm;
bool _running = false;
uint32_t _t0 = 0, _dur = 0;
};
Buton basımında raise() veya lower() çağrılır, teleopLoop her turda periodic() çağırır. Süre yöntemi limit switch koruması içermez; süreyi mekanizma ucuna çarpmayacak şekilde ayarlamak gerekir.
3. Encoder Varsa: PID Şablonu
Encoder, sliderın gerçek konumunu (tick sayısı) verir. Bununla bir hedef konuma gidilebilir: hata (hedef eksi mevcut konum) hesaplanır, motora hatayla orantılı bir güç verilir. Bu geri besleme döngüsü PID olarak bilinir.
probot-core encoder veya PID sağlamaz; aşağıdaki yalnızca bir başlangıç şablonu. Encoder okuma ve kazanç kalibrasyonu (kp, ardından gerekirse ki ve kd) size kalıyor.
class pid_slider_t {
public:
pid_slider_t(int rpwm, int lpwm) : _rpwm(rpwm), _lpwm(lpwm) {}
void init() { pinMode(_rpwm, OUTPUT); pinMode(_lpwm, OUTPUT); }
void setTarget(long ticks) { _target = ticks; }
void periodic() {
long pos = readEncoder(); // kendi encoder okumanız
float error = (float)(_target - pos);
float out = _kp * error; // basit P; ki, kd eklenebilir
out = constrain(out, -1.0f, 1.0f);
_drive(out);
}
void stop() { _drive(0.0f); }
private:
long readEncoder() { return 0; } // TODO: encoder tick sayısı döndür
void _drive(float speed) {
if (speed >= 0) { analogWrite(_rpwm, (int)(speed * 255)); analogWrite(_lpwm, 0); }
else { analogWrite(_rpwm, 0); analogWrite(_lpwm, (int)(-speed * 255)); }
}
int _rpwm, _lpwm;
long _target = 0;
float _kp = 0.0f; // kalibrasyon: önce sadece kp artırılır
};
Kalibrasyon kabaca şöyle: _kp sıfırdan başlanıp slider hedefe doğru hareket edene kadar küçük adımlarla artırılır. Hedefin etrafında salınıyorsa fazla; çok yavaş yaklaşıyorsa az. İnce ayar (salınımı söndürme, kalıcı hatayı kapatma) için ki ve kd terimleri eklenir. Bu ayar deneme gerektirir ve mekanizmaya özeldir.
LED Durumu
Kütüphane ESP32'deki NeoPixel LED'i otomatik yönetir; müdahale gerekmez. LED rengi robotun o anki durumunu gösterir. (Eski sürümlerdeki setColor / set / setBrightness API'si 0.3.0'da kaldırıldı; LED tamamen kütüphanenin kontrolündedir.)
Robota harici bir sinyal lambası (RSL) bağlanabilir: #define PROBOT_RSL_PIN <gpio> verilirse kütüphane o pini de yönetir — robot hareket edebilirken (otonom ya da teleop çalışırken) yanıp söner, hareket edemezken (durdurulmuş, Init evresi ya da acil durdurma) sabit yanar.
| Renk | Anlam |
|---|---|
| Mavi sabit | Robot açık, Driver Station bağlı değil |
| Mavi yanıp sönüyor | Driver Station bağlı, robot durduruldu; mod seçimi ve Init bekleniyor |
| Sarı sabit | Init yapıldı, Start bekleniyor |
| Sarı yanıp sönüyor | Otonom bitti; TeleOp için Init bekleniyor |
| Turuncu yanıp sönüyor | Otonom çalışıyor |
| Yeşil yanıp sönüyor | Teleop çalışıyor |
| Kırmızı yanıp sönüyor | Deadline miss, loop bloke oldu |
| Kırmızı sabit | Emergency stop aktif |
Varsayılan pin GPIO 3. Farklı bir pine bağlıysa #include <probot.h>'den önce tanımla:
#define NEOPIXEL_PIN 48
Batarya Ölçümü
Arayüzdeki batarya göstergesini besleyecek gerilimi kütüphaneye üç yoldan verebilirsin. Hiçbiri açık değilse gösterge "Veri yok" der. Bir yöntem seç; ADC ile INA aynı anda kullanılamaz (derlemede PB-E105 ile yakalanır).
1. Gerilim Bölücü + ADC
En ucuz yöntem: iki dirençle bataryayı ADC'nin okuyabileceği aralığa indirirsin.
BAT+ ──[100k]──┬──[22k]── GND
│
GPIO5 (ADC1) ── 100nF ── GND
#define PROBOT_BATTERY_ADC_PIN 5 // GPIO1-10 arası ŞART (ADC1)
#define PROBOT_BATTERY_R_TOP_K 100
#define PROBOT_BATTERY_R_BOT_K 22
Pin GPIO1-10 arasından seçilmeli: ADC2 pinleri (GPIO11-20) WiFi açıkken çalışmaz; yanlış pin derlemede PB-E104 ile yakalanır. 100k/22k, 3S LiPo'nun 12.6 V tepesini 2.27 V'a indirir (ADC'nin doğrusal bölgesi). Orta uca 100nF kondansatör koy; bölücüyü ana anahtarın sonrasına bağla ki robot kapalıyken pili süzmesin (~0.1 mA).
Okuma eFuse kalibrasyonlu, 8 örnek ortalama + yumuşatma ile ~%1-2 doğruluktadır. Multimetreyle fark görürsen PROBOT_BATTERY_TRIM ile ince ayar yap.
2. INA219 / INA226 I2C Sensörü
Daha hassas ve lehim istemez:
#define PROBOT_BATTERY_INA 226 // ya da 219
// opsiyonel: PROBOT_BATTERY_INA_ADDR / _SDA / _SCL
INA'nın shunt'ı üzerinden anlık akım da okunur: probot::io::battery::currentAmps(). Sensöre çalışma zamanında ulaşılamazsa PB-E306 uyarısı düşer ve gösterge "Veri yok"a döner.
INA'nın I2C bus'ını kullanıcı kodundan kullanma
Kütüphane INA'yı kendi görev döngüsünden okur; aynı Wire bus'ına robot kodundan ikinci bir cihaz (IMU, OLED...) bağlarsan okumalar karışabilir. Kendi I2C cihazın varsa ya batarya için ADC yöntemini seç ya da cihazını Wire1'e (ayrı pinler) taşı.
Shunt notu
Akım hesabı PROBOT_BATTERY_INA_SHUNT_MOHM (varsayılan 100) ile yapılır. INA226'nın ±81.92 mV shunt aralığı 100 mΩ ile ±0.82 A'da doyar — modülündeki shunt değerini (örn. 2 mΩ) tanımla. Gerilim ölçümü shunt'tan bağımsızdır.
3. Elle Besleme
Kendi ölçümün varsa gerilimi doğrudan verebilirsin:
probot::setBatteryVoltage(v);
ADC ya da INA tanımlıyken bunu kullanma; otomatik okuma değeri düzenli üzerine yazar.
Göstergeyi Okumak
Dashboard göstergesi son ~8 saniyenin ortalamasını gösterir. Logs → History grafiği ise örnekleri ortalamasız çizer; motor yükünde gerilim çöküşünü (sag) oradan izle. Hata durumları için: PB-E104 / PB-E105 / PB-E306.
WiFi Yapılandırması
Robot bir WiFi erişim noktası (AP) açar. Bu erişim noktasının adı, şifresi ve kanalı #include <probot.h>'den önce makrolarla tanımlanır. Sonrasında tanımlanırsa derleme hatasına yol açar.
Zorunlu Makrolar
#define PROBOT_WIFI_AP_PASSWORD "en_az_8_karakter"
#define PROBOT_WIFI_AP_CHANNEL 1 // 1, 6 veya 11 önerilir
Opsiyonel
#define PROBOT_WIFI_AP_SSID "RobotAdi" // tanımsız bırakılırsa "Probot-XXXXXX"
Tüm Makrolar
| Makro | Varsayılan | Açıklama |
|---|---|---|
PROBOT_WIFI_AP_SSID |
Probot-XXXXXX |
WiFi ağ adı. Tanımsız = MAC adresinden otomatik |
PROBOT_WIFI_AP_PASSWORD |
zorunlu | ≥8 karakter |
PROBOT_WIFI_AP_CHANNEL |
zorunlu | 1-13 serbest; varsayılan öneri 1, 6 veya 11 |
PROBOT_WIFI_AP_SSID_MAC_SUFFIX |
kapalı | SSID sonuna -XXXXXX ekler |
PROBOT_DS_TIMEOUT_MS |
10000 |
DS sessizlik timeout'u (ms) |
PROBOT_DS_TIMEOUT_FORCE_STOP |
1 |
1: timeout'ta STOP. 0: joystick nötr, loop sürer |
PROBOT_DS_OWNER_TIMEOUT_MS |
5000 |
Sahip client sessiz kalınca slot boşalma süresi (ms) |
PROBOT_INPUT_TIMEOUT_MS |
500 |
Joystick verisi kesilince eksen sıfırlama süresi (ms) |
PROBOT_CAPTIVE_PORTAL |
1 |
0: captive portal devre dışı |
PROBOT_WIFI_ENABLE_11B |
0 |
1: 802.11b aç (eski cihazlar, önerilmez) |
PROBOT_WIFI_PMF_REQUIRED |
0 |
1: PMF zorunlu, eski tabletlerle uyumsuz olabilir |
NEOPIXEL_PIN / NEOPIXEL_COUNT |
3 / 1 |
LED pin ve adedi |
NEOPIXEL_BRIGHTNESS |
32 |
LED parlaklığı (0-255) |
PROBOT_LOOP_DEADLINE_MS |
2000 |
Bu süreyi aşan tur halt-safe'e girer |
PROBOT_WDT_TIMEOUT_S |
8 |
Donanım watchdog; yalnız kütüphane task'ını izler, kullanıcı loop'u reboot ettirmez |
PROBOT_ESTOP_END_MS |
500 |
Acil durdurmada aktif modun stop hook'una tanınan süre; aşılırsa çip reboot eder |
PROBOT_ESTOP_ENABLE_PIN |
-1 |
Kütüphanenin sürdüğü enable GPIO'su (motor sürücü enable). Boot'ta HIGH, acil durdurmada LOW |
PROBOT_RSL_PIN |
-1 |
Sinyal lambası (RSL) pini: robot hareket edebilirken yanıp söner, edemezken sabit yanar |
PROBOT_BATTERY_ADC_PIN |
kapalı | Batarya ölçümü, yöntem 1: gerilim bölücünün orta ucu. ADC1 pini şart (GPIO1-10) — ADC2 WiFi açıkken çalışmaz [PB-E104] |
PROBOT_BATTERY_R_TOP_K / _R_BOT_K |
— | Bölücü dirençleri, kΩ (batarya tarafı / GND tarafı). 3S için öneri: 100k/22k → 12.6 V'ta 2.27 V |
PROBOT_BATTERY_INA |
kapalı | Batarya ölçümü, yöntem 2: I2C sensör — 219 ya da 226. ADC yöntemiyle birlikte kullanılamaz [PB-E105] |
PROBOT_BATTERY_INA_ADDR |
0x40 |
INA I2C adresi (0x08-0x77) |
PROBOT_BATTERY_INA_SDA / _SCL |
kart default'u | INA için I2C pinleri (ikisi birlikte verilir) |
PROBOT_BATTERY_INA_SHUNT_MOHM |
100 |
INA shunt değeri, mΩ; anlık akım hesabı için |
PROBOT_BATTERY_TRIM |
1.0f |
Multimetreyle ince ayar çarpanı (0.5-2.0) |
USER_LOOP_PERIOD_MS |
20 |
Loop çağrı periyodu (~50 Hz) |
PROBOT_ESTOP_ENABLE_PIN |
-1 |
Motor sürücü enable pini; acil durdurmada LOW çeker |
Pratik Senaryolar
Aynı kodu birden çok robota yüklemek. Tüm robotlar aynı SSID ile açılırsa ağlar birbirine karışır. PROBOT_WIFI_AP_SSID_MAC_SUFFIX her robotun SSID'sine kendi MAC adresinin son baytlarını ekler: "RobotAdi" → "RobotAdi-A1B2C3". Böylece her robot benzersiz görünür, kod aynı kalır.
#define PROBOT_WIFI_AP_SSID "RobotAdi"
#define PROBOT_WIFI_AP_SSID_MAC_SUFFIX
#include <probot.h>
Tezgahta Driver Station olmadan test. Varsayılanda Driver Station 10 saniye sessiz kalınca robot STOP'a geçer (PROBOT_DS_TIMEOUT_FORCE_STOP 1). DS bağlamadan motor veya sensör denemek için bunu 0 yaparsan robot STOP'a geçmez; bağlantı kesik olsa bile joystick nötrlenir ama loop dönmeye devam eder. Yarışmada 1 kalmalı, bu yalnızca test için.
#define PROBOT_DS_TIMEOUT_FORCE_STOP 0 // SADECE tezgah testi
#include <probot.h>
Donanımsal acil durdurma. BTS7960'ın EN (enable) pinlerini tek bir GPIO'ya bağlayıp bu pini PROBOT_ESTOP_ENABLE_PIN'e verirsen, kütüphane pini normalde HIGH (enable) tutar, acil durdurmada LOW çeker. Böylece motorlar yalnızca yazılımla değil, sürücü seviyesinde de kesilir; kod kilitlense bile motor durur.
#define PROBOT_ESTOP_ENABLE_PIN 25 // BTS7960 EN pinlerine bağlı GPIO
#include <probot.h>
LED'i kısmak veya pinini değiştirmek. Varsayılan parlaklık 32; gözü rahatsız ediyorsa düşür. LED farklı bir pine bağlıysa pini değiştir.
#define NEOPIXEL_BRIGHTNESS 16
#define NEOPIXEL_PIN 48
#include <probot.h>
Otonom
Otonom fazda joystick yok. Robot tamamen koda bağlı. Bunu yapmanın yolu: autonomousInit()'te başlangıç durumunu ayarla, autonomousLoop()'ta her turda ne yapacağına karar ver.
autonomousLoop ~50 Hz'de çağrılır, yani her tur yaklaşık 20 ms'de bir geliyor. İçinde delay(2000) gibi uzun bir bekleme varsa o tur 2 saniye boyunca dönmez; kütüphane bu durumu tespit eder ve deadline miss hatası oluşur. Deadline miss'te joystick sıfırlanır ve LED kırmızı yanıp söner; task öldürülmez, faz değişmez, tur dönünce hata kendiliğinden temizlenir. Daha fazla bilgi için Hatalar - PB-E301 Deadline Miss sayfasına bakılabilir.
Bunun yerine bekleme için millis() kullanılır: her turda geçen zamanı kontrol et, süre dolunca bir sonraki adıma geç.
Durum Makinesi
En basit otonom, sabit süreli fazlardan oluşan bir durum makinesi. Aşağıdaki örnek ileri gider, sola döner ve durur.
enum class Phase { FORWARD, TURN_LEFT, DONE };
static Phase phase;
static uint32_t t_ref;
void autonomousInit() {
phase = Phase::FORWARD;
t_ref = 0;
}
void autonomousLoop() {
if (t_ref == 0) t_ref = millis(); // ilk tur = Start anı
uint32_t elapsed = millis() - t_ref;
switch (phase) {
case Phase::FORWARD:
// motorLeft(0.6f); motorRight(0.6f);
if (elapsed > 2000) { phase = Phase::TURN_LEFT; t_ref = millis(); }
break;
case Phase::TURN_LEFT:
// motorLeft(-0.4f); motorRight(0.4f);
if (elapsed > 800) { phase = Phase::DONE; }
break;
case Phase::DONE:
// motorLeft(0); motorRight(0);
break;
}
delay(20);
}
void autonomousStop() { stopMotors(); }
static değişkenler program boyunca değerlerini korur. autonomousInit() her otonom başlangıcında çağrıldığı için bu değişkenleri burada sıfırla; yoksa bir önceki çalışmadan kalan değerle başlar. Zaman damgası ise Init'te alınmaz: Init ile Start arasında süre geçebilir ve Init'te robot kımıldamaz; bu yüzden t_ref, loop'un ilk turunda — yani Start anında — ayarlanır (t_ref == 0 kalıbı).
Mesafe ve Açıyla Hareket
Süre yerine mesafe cinsinden düşünmek daha kolaydır: "1 metre ileri, 90 derece sağa, 0.5 metre ileri". Encoder olmadan robot gerçek mesafeyi bilemez ama sabit güçte geçen süre kabaca sabit mesafeye karşılık gelir. İki katsayı deneyerek bulunur: 1 metrenin kaç saniye, 1 derecenin kaç saniye sürdüğü. Bu yöntemin adım listesine dayalı temel kurulumu Örnek Robot - Mesafe ve Açıyla Hareket sayfasında; aşağıda aynı fikir koordinatlara genişletiliyor.
Koordinata Gitme
Robotun başlangıç konumu ve yönü biliniyorsa, saha üzerinde koordinat noktaları tanımlanıp "şu noktaya git" denebilir. Robot her nokta için önce hedefe döner, sonra aradaki mesafeyi gider. Konum metre, yön derece cinsinden tutulur; yön 0, başlangıçta bakılan ileri yön, pozitif yön saat yönünde (sağa) artar.
Her nokta için iki şey hesaplanır: hedefe bakan yön ve aradaki mesafe. Yön, konum farkından atan2(dx, dy) ile bulunur; argüman sırası dx, dy, çünkü yön 0 ileri (+Y) yönüne karşılık gelir. Robotun o anki yönüyle hedef yön arasındaki fark, ne kadar dönüleceğini verir. normalizeDeg bu farkı -180 ile +180 arasına çeker; böylece robot 350 derece dönmek yerine kısa yoldan ters yöne 10 derece döner. Mesafe ise iki nokta arası düz çizgi uzunluğu (sqrt(dx² + dy²)).
void drive(float fwd, float turn) {
motorLeft(fwd + turn);
motorRight(fwd - turn);
}
const float DRIVE_POWER = 0.6f;
const float SEC_PER_METER = 2.3f;
const float TURN_POWER = 0.6f;
const float SEC_PER_DEGREE = 0.011f;
enum class Move { GO, TURN };
struct Step { Move type; float amount; };
struct Point { float x, y; };
Point waypoints[] = { { 1.0f, 1.0f }, { 1.0f, 2.0f }, { 0.0f, 2.0f } };
const int WP_COUNT = sizeof(waypoints) / sizeof(waypoints[0]);
float robot_x = 0.0f, robot_y = 0.0f, robot_deg = 0.0f;
Step plan[2 * WP_COUNT];
int step_count = 0, step_i = 0;
uint32_t step_start = 0, step_dur = 0;
float normalizeDeg(float d) {
while (d > 180.0f) d -= 360.0f;
while (d < -180.0f) d += 360.0f;
return d;
}
void buildPlan() {
step_count = 0;
float x = robot_x, y = robot_y, heading = robot_deg;
for (int i = 0; i < WP_COUNT; i++) {
float dx = waypoints[i].x - x;
float dy = waypoints[i].y - y;
float target = degrees(atan2(dx, dy));
plan[step_count++] = { Move::TURN, normalizeDeg(target - heading) };
plan[step_count++] = { Move::GO, sqrt(dx * dx + dy * dy) };
x = waypoints[i].x; y = waypoints[i].y; heading = target;
}
}
void startStep(int i) {
step_i = i; step_start = millis();
if (i >= step_count) { drive(0.0f, 0.0f); return; }
Step s = plan[i];
if (s.type == Move::GO) {
step_dur = (uint32_t)(fabs(s.amount) * SEC_PER_METER * 1000); // saniye -> ms
drive(s.amount > 0 ? DRIVE_POWER : -DRIVE_POWER, 0.0f);
} else {
step_dur = (uint32_t)(fabs(s.amount) * SEC_PER_DEGREE * 1000); // saniye -> ms
drive(0.0f, s.amount > 0 ? TURN_POWER : -TURN_POWER);
}
}
void autonomousInit() { buildPlan(); startStep(0); }
void autonomousLoop() {
if (step_i < step_count && millis() - step_start >= step_dur) startStep(step_i + 1);
delay(20);
}
void autonomousStop() { stopMotors(); }
Bu yöntem tank şaside makul çalışır; mecanumda kayma yüzünden daha az güvenilir. Yine de tahmine dayanır ve her adımda biraz sapma birikir. Sapmayı gerçekten ölçüp düzeltmek encoder ve jiroskop gerektirir; bu rehber bu katmanlara girmiyor.
Hareketi Mekanizmalarla Birleştirmek
Gerçek bir otonom yalnızca sürmez; mekanizmaları da çalıştırır. Subsystem deseni bunu kolaylaştırır: her faz hem sürüşü hem mekanizmayı kumanda eder, sensör tabanlı subsystem'ler her turda periodic() ile güncellenir. Aşağıda robot ileri gider, sliderı hedefe kaldırır ve intake'i çalıştırır.
enum class Phase { DRIVE_TO, RAISE, INTAKE, DONE };
static Phase phase;
static uint32_t t_ref;
void autonomousInit() {
phase = Phase::DRIVE_TO;
t_ref = 0;
}
void autonomousLoop() {
if (t_ref == 0) t_ref = millis(); // ilk tur = Start anı
uint32_t elapsed = millis() - t_ref;
slider.periodic(); // PID slider hedefe her turda yaklaşır
switch (phase) {
case Phase::DRIVE_TO:
drive(0.6f, 0.0f);
if (elapsed > 1500) { drive(0.0f, 0.0f); slider.setTarget(800); phase = Phase::RAISE; t_ref = millis(); }
break;
case Phase::RAISE:
if (elapsed > 1000) { phase = Phase::INTAKE; t_ref = millis(); }
break;
case Phase::INTAKE:
intake.run();
if (elapsed > 1200) { intake.stop(); phase = Phase::DONE; }
break;
case Phase::DONE:
break;
}
delay(20);
}
void autonomousStop() { stopMotors(); }
Her faz bir komut gibi davranır: bir koşul sağlanınca (süre dolunca veya hedefe ulaşınca) sıradakine geçer. Karmaşık otonomlar bu desenin uzamış hâli.
Robot Durumu (İleri Seviye)
Robotun o anki fazını ve durumunu kod içinden okumak gerekirse:
auto s = probot::robot::state().read();
| Alan | Tip | Açıklama |
|---|---|---|
s.status |
Status::INIT/START/STOP |
Son kabul edilen komut |
s.phase |
Phase::STOPPED/AUTO_INIT/AUTO_RUN/TELEOP_INIT/TELEOP_RUN/TRANSITION |
Mevcut faz (hook'u tamamlanmış gerçek evre) |
s.selectedMode |
OpMode::AUTO/TELEOP |
Seçili mod (Otonom / TeleOp) |
s.deadlineMiss |
bool | Halt-safe aktif mi |
s.autoPeriodSeconds |
int32_t | Otonom süresi (sn) |
s.clientCount |
int32_t | Bağlı DS istemcisi |
s.batteryVoltage |
float | Batarya ölçümünden gelen gerilim; 0.0 = veri yok. Bkz. Batarya Ölçümü |
Aynı durum robot dışından da okunabilir (kendi izleme aracını ya da DS istemcisini yazanlar için): GET /getState şu JSON'u döner: {"status":N,"phase":N,"selectedMode":"auto"|"teleop","autoPeriodSeconds":N,"autoRemainingMs":N,"batt":V.V,"estop":b} — estop alanı acil durdurma kilidini, batt alanı batarya ölçümünden gelen pil gerilimini gösterir (0.0 = veri yok; bkz. Batarya Ölçümü). WebSocket bağlantısında robot aynı bilgiyi 'S' çerçevesiyle kendisi push eder (alanlar /getState + /health birleşimi; değişiklikte ~250 ms içinde, değişiklik yoksa ~1.25 sn'de bir heartbeat olarak). Endpoint'lerin ve çerçeve formatlarının tam listesi core deposundaki API.md'de.