C# arayüzünde Pixhawk verisi bozuk, önce baytları say
C# yer kontrol istasyonunda Pixhawk verisi bozuk çıkıyorsa sorun hatta değil, MAVLink çerçevesini okuma biçimindedir. Belirti, sebep ve çalışan okuma kodu.
Kendi yer kontrol istasyonunu C# ile yazdın. Port açılıyor, ışık yanıyor, baytlar akıyor. Ama ekrandaki irtifa bir 12 metre, bir 3.400 metre; yaw değeri de arada bir çılgına dönüyor.
Hat bozuk değil, okuma biçimi bozuk. Aynı bayt akışını iki farklı kurguyla okursan biri 5.440 paket çıkarır, diğeri aynı akıştan 181 tane fazladan paket uydurur.
Ekranda ne oluyor
Belirti hep aynı sırayla gelir. Önce her şey düzgün görünür, birkaç saniye sonra bir alan sıçrar, sonra düzelir, sonra yine sıçrar. Uygulamayı kapatıp açınca bir süre daha düzgün çalışır.
Bu desen, ayrıştırıcının akışın içinde yanlış bir yerden çerçeve başlatıp bir sonraki gerçek çerçevede kendini toparlamasından gelir.
Çerçevenin nerede bittiğini başlığı söyler
Otopilottan gelen akış, arka arkaya dizilmiş MAVLink çerçevelerinden oluşur. Her çerçeve 0xFD baytıyla başlar, ama boyu sabit değildir.
Başlık 10 bayttır ve gövde uzunluğu başlığın ikinci baytında (LEN) gelir. Toplam boy 12 + LEN bayttır; imzalı çerçevede buna 13 bayt eklenir. ArduCopter'dan alınan 20 saniyelik örnek bir akışta paket boyu 14 ile 56 bayt arasında değişiyor.
Buradan ilk kural çıkıyor: çerçeveyi ancak kendi uzunluğu kadar atlayabilirsin. Sabit boy varsayan bir okuma ikinci pakette kayar ve bir daha toparlanmaz.
Gövdenin içindeki bayt da 0xFD olabilir
Yaygın kurgu şudur: tamponda 0xFD baytını ara, bulduğun yeri paket başı say. Oysa gövde ham ikili veri taşır; içinde 0xFD baytı geçmesini engelleyen hiçbir şey yoktur.
Aşağıdaki şekilde aynı baytları iki kurguyla okuyorsun. Sarı kenarlı FD'ye bak: 0xFD arayan okuma orada durur, çerçeveden ilerleyen okuma LEN'den hesapladığı boy kadar atlar ve onu hiç görmez.
Aynı baytlar, iki okuma kurgusu
Ayrımı yapan tek şey CRC doğrulamasıdır. CRC tutuyorsa o gerçekten bir çerçeve başıdır; tutmuyorsa gövde içindeki rastgele bir bayta denk gelmişsindir ve yalnızca 1 bayt kaydırırsın.
ArduCopter'dan alınan 20 saniyelik, 190.653 baytlık bir akışta 0xFD baytı 5.622 kez geçer ve bunların 5.440'ı gerçek çerçeve başıdır. Aynı akışta 0xFE de 143 kez geçer, yani v1 başlangıcı arayan kod bu kadar kez daha yanlış tetiklenir. Oran akışın içeriğine göre değişir, sıfır olmaz.
Read çağrısı istediğin kadar bayt döndürmez
İkinci tuzak okuma katmanında. C# tarafında hem SerialPort hem NetworkStream, tamponun tamamını doldurmadan da geri dönebilir.
Aynı akışı 256 baytlık tamponla okursan 803 çağrının 202'si (%25,2) 256'dan az bayt döndürür; en küçük dönüş 25 bayt, ortalama 237,4 bayttır. Çerçevelerin %13,3'ü de iki ayrı okuma çağrısına bölünür.
Aşağıdaki şekilde sarı çerçeveye bak: okuma sınırı onu ikiye bölüyor ve ilk parça bir sonraki okumayı bekliyor.
Read çağrısı çerçeveyi ortasından keser
Dönen değeri yok sayıp tamponun tamamını işleyen kod, okumaların dörtte birinde önceki turdan kalan baytları ikinci kez ayrıştırır. Ortaya çıkan tekrar paketleri hat gürültüsüne benzer, oysa kaynak kendi kodundur.
Okuma döngüsünü iki adımda düzelt
İki düzeltme yeter. Birincisi, Read çağrısının döndürdüğü sayıyı kullan.
// Read her cagrida istedigin kadar bayt donmez.
// Donen degeri yok saymak, tamponun eski kismini tekrar islemek demektir.
var tampon = new byte[256];
while (calisiyor)
{
int adet = akis.Read(tampon, 0, tampon.Length);
if (adet <= 0) break; // hat kapandi
okuyucu.BaytEkle(tampon, adet); // yalnizca gercekten gelen adet kadar
}İkincisi, gelen baytları biriktiren ve yalnızca CRC'si tutan çerçeveleri tüketen bir okuyucu koy. Yarım kalan kuyruk bir sonraki okumaya devredilir.
// MAVLink baytlarini cerceve sinirindan ilerleyerek oku.
// Tampon dolduktan sonra ne kadar tam cerceve varsa onu tuketir,
// yarim kalan kuyrugu bir sonraki okumaya devreder.
class CerceveOkuyucu
{
readonly byte[] _tampon = new byte[8192];
int _dolu;
public void BaytEkle(byte[] gelen, int adet)
{
if (_dolu + adet > _tampon.Length) _dolu = 0; // hat bozulmus, bastan basla
Buffer.BlockCopy(gelen, 0, _tampon, _dolu, adet);
_dolu += adet;
int p = 0;
while (_dolu - p >= 12)
{
if (_tampon[p] != 0xFD) { p++; continue; }
int govde = _tampon[p + 1];
int imza = (_tampon[p + 2] & 0x01) != 0 ? 13 : 0;
int boy = 12 + govde + imza;
if (_dolu - p < boy) break; // cerceve henuz tamamlanmadi
int mesajId = _tampon[p + 7] | (_tampon[p + 8] << 8) | (_tampon[p + 9] << 16);
if (CrcTutuyor(_tampon, p, govde, mesajId))
{
CerceveGeldi(mesajId, _tampon, p + 10, govde);
p += boy; // tam cerceve kadar ilerle
}
else
{
p++; // tutmadi: yalnizca 1 bayt kaydir
}
}
_dolu -= p;
Buffer.BlockCopy(_tampon, p, _tampon, 0, _dolu); // kuyrugu basa al
}
bool CrcTutuyor(byte[] d, int bas, int govde, int mesajId)
{
if (!CrcEkstra.TryGetValue(mesajId, out byte ekstra)) return false;
ushort crc = 0xFFFF;
for (int k = 1; k < 10 + govde; k++) crc = Biriktir(d[bas + k], crc);
crc = Biriktir(ekstra, crc);
ushort gelen = (ushort)(d[bas + 10 + govde] | (d[bas + 11 + govde] << 8));
return crc == gelen;
}
static ushort Biriktir(byte b, ushort crc)
{
byte t = (byte)(b ^ (byte)(crc & 0xFF));
t ^= (byte)(t << 4);
return (ushort)((crc >> 8) ^ (t << 8) ^ (t << 3) ^ (t >> 4));
}
}CrcEkstra tablosu her mesaj kimliği için bir bayt tutar ve MAVLink tanımlarından üretilir. Tabloyu elle yazma; üretilmiş bir tablo kullan, yoksa doğrulama her mesajda düşer.
Bu okuyucuyla yukarıdaki akış baştan sona senkron kalır ve 5.440 çerçevenin hepsi gelir. Tamponda 0xFD arayan kurgu da aynı 5.440 çerçeveyi bulur, ama bunlara ek olarak 181 kayıt daha çıkarır ve o kayıtlar ekrana rastgele alan değerleriyle düşer.
Örnek: Yarışma alanında yer istasyonunun harita ekranında drone'u izliyorsun; irtifa kutusu birkaç saniyede bir 3.400 metreye sıçrayıp geri geliyor. Hakem ekranı sorunca telemetri modülünü ya da anteni suçlamak kolay, ama değer sıçrayıp düzeliyor, sonra yine sıçrıyorsa önce okuma döngüne bak. Yukarıdaki okuyucuyu koyup sıçramalar kesildiyse sorun hiçbir zaman hatta değildi.
TCP ile seri hat arasındaki fark
Yukarıdaki sayılar TCP bağlantısından. Çerçeveleme mantığı iki hatta da aynıdır; değişen, baytların hatta nasıl geldiğidir.
- TCP tarafında bayt kaybı olmaz, sıra bozulmaz. Gördüğün her senkron kaybı senin kodundandır.
- Seri hatta gürültü, baud sapması ve akış kontrolü eksikliği baytları gerçekten düşürebilir. Orada CRC hem senin hatanı hem hattın hatasını yakalar.
- SERIAL1_BAUD parametresi 57 yazıyorsa hat 57.600'de çalışıyor demektir; değer bine bölünmüş saklanır.
Hata Mesajları ve Anlamları
Bu üç metin .NET tarafında birebir bu şekilde çıkar. Hangisini gördüğün, sorunun hangi katmanda olduğunu söyler.
System.UnauthorizedAccessException: Access to the port 'COM3' is denied.
Port başka bir program tarafından açık. Mission Planner ya da önceki çalıştırmadan kalmış bir örnek hattı tutuyordur. Aynı seri portu iki uygulama paylaşamaz.
System.IO.IOException: Unable to read data from the transport connection: Connection timed out.
Bağlantı kuruldu ama okuma süresi doldu. Baud eşleşmiyorsa ya da karşı taraf MAVLink konuşmuyorsa bu metni görürsün. Önce SERIALx_PROTOCOL değerine bak.
System.Net.Sockets.SocketException: Connection refused
TCP tarafında karşı uçta dinleyen yok. Bağlanmaya çalıştığın program kapanmıştır ya da port numarası yanlıştır. Bu mesajı gördüğünde ayrıştırıcıyı kurcalamanın anlamı yok, henüz bayt gelmiyor.
Bu konunun devamı
Yukarıdaki okuyucu, çalışan bir yer kontrol istasyonunun en alt katmanı. Üstüne arayüz, kayıt ve komut gönderme katmanı gelir.
Bunları uçtan uca kuran ders Sıfırdan İleri Seviyeye C# eğitimi içinde. Orada firmware'in karar sırası, log imzaları ve kart bazlı farklar da var.
Softivation YKİ kütüphanesi: C# ile kendi yer kontrol istasyonunu yazarken MAVLink bağlantısı, Türkçe dokümantasyon ve hazır test arayüzüyle başla.
Bu yazı işinize yaradı mı?
Bu konuyu baştan sona öğrenin
Sezon: altı eğitimin hepsi 9 ay, ayda ₺790. Lise ve üniversite öğrencisine ayda ₺390, üye kodunuzla ilk ay ₺351. İstediğiniz ay bırakırsınız. Sezon'a başlayın · Öğrenciyim