Aşama 4: Firmware ve Cloud Yazılımı Geliştirme
V’nin sağ tarafına hoş geldiniz! Ürün geliştirmenin klasik V-modelinde sol taraf tasarımla ilgilidir (bunu önceki aşamalarda hallettik), sağ taraf ise uygulama ve testle. Aşama 4’te, IoT cihazınızın üzerinde çalışan yazılım olan firmware’i ve ürününüzü birlikte hayata geçiren cloud yazılımını (sunucular, veritabanları, API’ler, kullanıcı arayüzleri) geliştirmeye dalıyoruz. Devre kartına bir kez basıldıktan sonra görece esnekliğini yitiren donanım geliştirmenin aksine, yazılım geliştirme son derece yinelemeli ve çeviktir. Hatta Aşama 3’te donanım tasarımınızı test ederken muhtemelen ufak tefek firmware parçaları yazmıştınız bile. Şimdi bunu ciddi biçimde hızlandırmanın zamanı. Yazılımın güzelliği kil gibi olması — anında şekil vermesi ve değiştirmesi kolay — bu sayede hızla yineleyip sürekli iyileştirebiliriz. Bu aşama, kalan donanım işleriyle paralel yürüyecek; iki taraf gerektikçe birbirini besleyecek.
Aşama 4’ü iki ana bölümde düşünebilirsiniz: önce yazılım mimarisi ve tasarımının planlanması (sağlam bir temel atmak için), ardından yazılım oturana kadar yap–test et–tekrarla döngüsüyle ilerleyen geliştirme sprint’leri. Hadi parçalayalım.
4.1 Yazılım Mimarisi ve Tasarımı
Herkes yarın yokmuş gibi kod yazmaya girişmeden önce, hem cihazdaki firmware hem de cloud bileşenleri için yazılım mimarisinin ana hatlarını çizmek akıllıca olur. Bunu bir yol gezisi planlamak gibi düşünün. Haritasız yola çıkmazsınız; benzer şekilde, binlerce satır kod yazmadan önce ana bileşenleri ve birbirleriyle nasıl etkileştiklerini kabaca çizeriz. Bu, Aşama 2’de yaptığımız sistem mimarisinin benzeri, ama bu kez kod seviyesinde. İyi bir yazılım mimarisi planlaması sizi ileride o korkunç “spagetti kod” canavarından kurtarır. Bu fazda oturtulması gereken temel şeyler şunlar:
Firmware Mimarisi (Katmanlı Yaklaşım ve RTOS Değerlendirmeleri)
Gömülü kodunuzun yapısını katmanlara ayırarak tanımlayın. Firmware’de yaygın bir yaklaşım katmanlı mimaridir; kodu mantıksal olarak düzenlemeye ve bakımını kolaylaştırmaya yardımcı olur. Örneğin en alt seviyede bir Board Support Package (BSP) ya da donanım soyutlama katmanı olabilir — bu katman mikrodenetleyicinize ve donanım çevre birimlerinize özgü düşük seviyeli sürücüleri içerir. Onun üzerinde sensörleriniz ve aktüatörleriniz için cihaz sürücüleri veya modüller olabilir (sıcaklık sensörü okumak ya da LED sürmek gibi işleri halleden kısımlar). Sürücülerin üzerinde bir servisler katmanı olabilir — burada iletişim protokolleri, veri işleme algoritmaları ya da ham veriyi işleyen diğer mantıklar yaşar. Ve en tepede cihazınızın beyni olan uygulama katmanı vardır: ürününüzün amacını gerçekleştirmek için her şeyi birbirine bağlar (örneğin sensör değerlerini okumak, kararlar almak, veriyi cloud’a göndermek ve komutlara tepki vermek). Bu katmanları görselleştirmek için basit bir diyagram çizmeyi düşünün (UML bileşen diyagramı, hatta beyaz tahtada kutular ve oklar bile olur). Bu, ekipteki herkesin sorumlulukların ayrımını anlamasına yardımcı olur — temeli, iskeleti ve çatısıyla bir ev inşa etmek gibi, her katmanın kendi işi vardır.
Firmware tarafındaki bir diğer büyük mimari karar, RTOS (Real-Time Operating System) kullanıp kullanmamak. RTOS, cihazınızda birden çok görevi (zamanlama yoluyla) sanki aynı anda çalıştırabilen küçük bir işletim sistemine sahip olmak gibidir. Cihazınız aynı anda tek bir iş yapıyorsa RTOS’a ihtiyacınız olmayabilir. Ama çoğu IoT cihazında aynı anda dönen çok iş vardır — örneğin sensörleri periyodik okumak, gelen mesajları dinlemek (cloud’dan veya kullanıcı girdisinden), bir ekranı veya LED’i güncellemek ve ağ iletişimini yürütmek. RTOS ile firmware’inizi bir dizi task ya da thread olarak tasarlayabilirsiniz. Mesela bir task’i sensör örneklemeye, bir başkasını iletişime (veri göndermek veya komut almak), bir diğerini de rutin bakım işlerine ya da kullanıcı arayüzü güncellemelerine ayırabilirsiniz. Mimari tasarımınızın parçası olarak bu task’leri ve birbirleriyle nasıl haberleşeceklerini ana hatlarıyla belirleyin — yaygın mekanizmalar arasında message queue’lar, event flag’ler veya mutex’lerle korunan paylaşımlı veriler (çakışmaları önlemek için) sayılabilir. Bunu erkenden kararlaştırmak, firmware’inizin her şeyin her şeye bağlı olduğu bir kod yumağına (nam-ı diğer spagetti kod) dönüşmemesini sağlar. Bunun yerine net arayüzleriniz olur: belki sensör task’i yeni okumaları bir queue’ya koyar, iletişim task’i de onları alıp cloud’a gönderir. Bu arayüzleri şimdi net tasarlarsanız kod yazmak çok daha pürüzsüz olur ve sıra firmware’de hata ayıklamaya ya da genişletmeye geldiğinde kendinize teşekkür edersiniz.
Firmware için bu kadar mimari yaygara niye? Çünkü ileride hayatınızı kolaylaştırıyor. İyi bir katmanlı tasarım ve task yapısıyla, bir parçayı (mesela bir sensör sürücüsünü değiştirmek ya da bir algoritmaya ince ayar çekmek) diğer her şeyi bozmadan değiştirebilirsiniz. Ayrıca birden çok geliştiricinin birbirinin ayağına basmadan birlikte çalışmasını kolaylaştırır — bu parçaların nasıl arayüzleneceğinde (örneğin bir sensör okuma mesajının veri formatında) anlaştığınız sürece, biri sensör sürücüsünü geliştirirken diğeri cloud iletişim kodu üzerinde çalışabilir. Kısacası, buradaki azıcık planlama ileride bir sürü acıdan kurtarır.
Cloud Mimarisi (Sunucu, Veritabanı ve Uygulama Yapısı)
Şimdi cloud tarafına bakalım. IoT cihazı tek başına yaşamaz — genellikle bir cloud servisiyle veya sunucuyla konuşur. Burada da benzer bir mimari düşünce gerekiyor: cloud yazılımınızın ana bileşenlerini kabaca çizin. Örneğin, cihazlardan veri alan ve aynı zamanda kullanıcı uygulamalarından gelen istekleri karşılayan bir API sunucunuz olabilir. Muhtemelen cihaz verilerini (sensör okumaları, cihaz durumları, kullanıcı bilgileri vb.) sakladığınız bir veritabanı olacaktır. Veri işleme veya analitik yapan ek servisler ya da microservice’ler de olabilir — örneğin veriyi anomaliler için analiz etmek, belirli koşullar sağlandığında uyarı veya aksiyon tetiklemek (mesela bir sıcaklık okuması çok yüksekse e-posta göndermek) ya da raporlama için toplulaştırmalar yapmak. Bunun üstünde bir de istemci uygulamalarını düşünün: belki kullanıcıların etkileşime girdiği bir web panosu veya mobil uygulama. Bu istemciler, veriyi göstermek ve kullanıcı komutlarını iletmek için (genellikle API üzerinden) sunucuyla haberleşecek (mesela kullanıcının telefonundan bir düğmeye basıp IoT ampulünü açması gibi).
Bu ekosistemin bir diyagramını çizmek faydalıdır — C4 modeli (Client-Container-Component-Code) yazılım mimarisi diyagramlamada popüler bir yöntemdir. En azından Container seviyesini çizin: cihazı, sunucuyu, veritabanını ve istemci uygulamayı gösterin, aralarındaki veri akışını işaretleyin. Örneğin diyagramınızda Cihaz’dan Cloud API’ye bir ok (“sensör verisini HTTP/MQTT ile gönderir”), Cloud’dan Veritabanı’na bir ok (“veriyi saklar”), İstemci Uygulama’dan Cloud’a bir ok (“kullanıcı veri ister veya komut gönderir”) olabilir ve bu böyle devam eder. Bu görsel harita, herkesin (firmware geliştiricileri, cloud mühendisleri, frontend geliştiricileri vs.) her şeyin nasıl bağlandığına dair net bir zihinsel modele sahip olmasını sağlar. Elinizde böyle bir diyagram varken tasarım sorunlarını tartışmak ve erkenden yakalamak çok daha kolaydır. Belki bu aşamada ek bir bileşene ihtiyacınız olduğunu keşfedersiniz — örneğin çok sayıda cihaza ölçeklenmeyi kaldırmak için bir message broker veya IoT platform servisi. Bunu kodun derinliklerine gömülmüşken değil, şimdi düşünmek çok daha iyi.
Bir konu daha: cloud tasarımınızda ölçeklenebilirlik ve güvenilirliği düşünün. Eninde sonunda 100.000 cihazın bağlanmasını bekliyorsanız, seçtiğiniz veritabanı ve sunucu tasarımı bunu kaldırabilir mi? Bir sunucu çökerse ne olur (yedekliliğiniz var mı)? Bu detaylar sonraki bir aşamada daha da olgunlaştırılabilir, ama üst seviye tasarımın bu tür büyük kaygıları en baştan hesaba katması gerekir. Örneğin her şeyi sıfırdan inşa etmek yerine bir cloud IoT platformu (AWS IoT, Azure IoT Hub vb.) kullanmaya karar verebilirsiniz — bu da mimari kararınızın bir parçası olur.
İletişim Protokolleri ve Veri Formatları (Cihaz–Cloud İletişimi)
Bir IoT ürünü doğası gereği cihaz ile cloud arasında iletişim içerir. Yazılım tasarımının bir parçası da birbirleriyle nasıl ve hangi dilde konuşacaklarına karar vermektir (protokoller ve veri formatları). IoT’de birkaç yaygın protokol seçeneği vardır: örneğin cihazınız MQTT mi, HTTP/REST mi, CoAP mı, yoksa WebSocket gibi bir şey mi kullanacak? Her birinin artıları ve eksileri var. MQTT, IoT için harika bir hafif publish/subscribe protokolüdür; verimlidir ve güvenilmez ağlarda iyi çalışır — birçok IoT sistemi cihazdan cloud’a mesajlaşma için onu kullanır. HTTP/REST çok yaygındır ve test etmesi kolaydır (standart web araçlarıyla dürtebilirsiniz), ama mesaj başına biraz daha fazla ek yükü olabilir. CoAP ise hafif bir HTTP’ye benzeyen, genellikle kısıtlı ağlarda kullanılan özelleşmiş bir IoT protokolüdür. İşin sırrı, cihazınızın yeteneklerine ve cloud altyapınıza uyan birini seçmek. Örneğin süper kısıtlı bir cihazınız varsa (pille çalışan 8-bit bir mikrodenetleyici), MQTT veya CoAP muhtemelen HTTP’den daha iyidir. Cihazınız hayli yetenekliyse (Linux veya benzeri çalıştırıyorsa), HTTP/REST gayet yeterli ve daha dolambaçsız olabilir.
Protokol seçildikten sonra mesajlar için veri formatını tasarlayın. Birçok proje JSON kullanır çünkü insan tarafından okunabilir ve çalışması kolaydır (çoğu dilde JSON parser vardır). Örneğin JSON formatında bir sıcaklık okuma mesajı şöyle görünebilir:
{ "device_id": "ABC123", "timestamp": 1692124800, "temperature_c": 22.5 }
Alanları gayet net ortaya koyuyor. Gerçi JSON biraz laf kalabalığı yapar; cihazınızın veriyi süper verimli göndermesi gerekiyorsa (binlerce mesaj veya çok düşük bant genişliği düşünün), byte’lardan tasarruf etmek için ikili bir format (Protocol Buffers, CBOR ya da özel bir binary şema gibi) kullanabilirsiniz. Buradaki tasarım tercihi hem firmware’i hem cloud’u etkiler: iki tarafın da protokol ve format üzerinde anlaşması şart. Bu biraz sohbete başlamadan önce ortak bir dilde anlaşmak gibi — cihaz MQTT+JSON “konuşuyorsa” ama sunucu başka bir şey bekliyorsa, birbirlerini anlamazlar.
Bu aşamada işe yarayan bir araç, bir sequence diagram çizmek ya da sadece örnek bir iletişim akışını yazıya dökmektir. Örneğin: “Cihaz bir sensör okuması alır, bir JSON payload’u oluşturur, MQTT ile X topic’ine gönderir; cloud broker’ı alır, cloud servisi işler ve onaylar, sonra veritabanına kaydeder; daha sonra kullanıcı uygulama üzerinden veri ister, cloud API veritabanından çeker ve uygulamaya JSON döner.” Bu akışı yazmak, herkesin (özellikle firmware geliştiricisi ile cloud geliştiricisinin) verinin nasıl dolaştığı konusunda kelimenin tam anlamıyla aynı sayfada olmasını sağlar. “Ha, ben UNIX timestamp’in saniye cinsinden olduğunu sanıyordum, milisaniye değil” ya da “Veriyi sıkıştırıyor muyduk, sıkıştırmıyor muyduk?” gibi iletişim kazalarını önleyebilir. Bunları şimdi, tasarımda yakalayın; sonradan hata ayıklama zamanından tasarruf ettirir.
Kodlama Standartları ve Ekip Araçları (Kalite Baştan Başlar)
Şimdi de kodu nasıl yazacağımızdan ve nasıl iş birliği yapacağımızdan bahsedelim, çünkü tüm ekibin ortak pratikleri izlemesi son derece önemli. Sistemin her parçası için kodlama standartlarında anlaşın. Gömülü C geliştirmede birçok ekip MISRA-C kurallarını benimser — bu, C dilinde güvensiz veya hataya açık yapılardan kaçınmayı amaçlayan bir kurallar setidir (aslen otomotiv sektöründen çıkmıştır ama her türlü kritik firmware’de işe yarar). Firmware’de C++ kullanıyorsanız, modern C++‘ın hangi özelliklerine izin verilip hangilerinin yasaklanacağına karar verebilirsiniz (gömülü sistemlerde işleri güvende tutmak için — örneğin çalışma zamanında dinamik bellek ayırmadan kaçınmak gibi). Cloud tarafındaki Python gibi yüksek seviyeli diller için muhtemelen PEP 8 stil rehberini izlersiniz; JavaScript/TypeScript için belki Airbnb stiliyle bir linter ya da standart neyse onu kullanırsınız. Amaç kılı kırk yarmak değil; tutarlılığı sağlamak ve yaygın tuzakları yakalamak. Herkes tutarlı bir stilde yazdığında, kod tabanı yamalı bohça değil tek bir ürün gibi hissettirir ve ekipteki bir başkasının yazdığı kodu okumak ve bakımını yapmak kolaylaşır.
Kodlama stiline ek olarak, ekibin kullanacağı araçları ve altyapıyı seçin. Buna versiyon kontrolü ve repository kurulumu dahil (neredeyse kesinlikle Git — hâlâ kullanmıyorsanız mutlaka başlayın, çünkü iş birliğine dayalı kodlamanın belkemiği). Branch stratejisine karar verin: basit bir trunk-based development mi, yoksa feature branch’li GitFlow mu, vs.? Ayrıca sprint’lerinizdeki görevleri ve user story’leri yönetmek için henüz kurmadıysanız bir issue takip sistemi kurun (Jira, Trello, Asana veya GitHub Issues gibi). Continuous Integration (CI) pipeline’ını kurmanın da tam zamanı: bir CI servisi seçin (GitHub Actions, GitLab CI, Jenkins, CircleCI vb.) ve her değişiklik push’landığında kodunuzu otomatik derleyip test edecek şekilde yapılandırın. CI’dan bir sonraki bölümde daha çok bahsedeceğiz, ama mimari aşaması, tüm geliştiricilerin bu araçlara erişimi olduğundan ve nasıl kullanılacağını bildiğinden emin olduğunuz yerdir.
“Kod kültürünün” bir parçası da code review pratikleri gibi şeylere karar vermektir — örneğin, her pull request en az bir başka geliştiricinin onayını gerektirecek mi? Bu beklentileri erken koymak sağlıklıdır. Biraz resmi gelebilir, ama sizi birinin gözden geçirilmemiş kodunun çirkin bir bug ya da güvenlik açığı sokması ve diğerlerinin bunu çok sonra fark etmesi gibi senaryolardan kurtarır. “Her zaman code review yaparız, stil rehberine uyarız ve merge’den önce testleri çalıştırırız” diye anlaşarak, ilk günden bir kalite zihniyeti aşılarsınız. Modern geliştirme, kodlama dehası kadar ekip iş birliği ve disiplin işidir.
Son olarak, unutmayın ki bu Aşama 4.1 planlaması kimsenin dönüp bakmayacağı şık diyagramlar çizmekle ilgili değil — ekibe ortak bir anlayış ve teknik bir yol haritası kazandırmakla ilgili. Yol gezisi rotasını planlayıp çantaya harita koymak gibi: büyük dönüşlerin nerede olduğunu ve nasıl ilerleyeceğinizi bilirsiniz, ama mola yerlerinde veya sapaklarda hâlâ esnek olabilirsiniz. Bu mimari tasarıma makul bir süre ayırın, ama mükemmeliyet peşinde koşmayın ya da en son fonksiyona kadar detaylandırmayın. İnsanların koordineli biçimde kod yazmaya başlayabileceği kadar tasarım yeterli. Bu temel atıldıktan sonra, geliştirmeye güvenle atlayabilirsiniz.
4.2 Geliştirme Sprint’leri – Yinele ve Fethet
Mimari plan elde, şimdi eğlence başlıyor — kod yazmak, test etmek ve IoT ürününüzün gerçekten bir şeyler yaptığını görmek! Buna yinelemelerle yaklaşıyoruz; genellikle Agile metodolojisindeki gibi sprint’lerle. Sprint, ekibin belirli bir özellik veya iyileştirme setini sonunda teslim etmeyi taahhüt ettiği kısa, sabit uzunlukta bir döngüdür (yaygın olarak 2 hafta, ama ekibinizin tercihine göre 1 hafta veya 4 haftaya kadar olabilir). Fikir, “tüm yazılımı geliştir” gibi devasa bir işi, istikrarlı biçimde üstesinden gelebileceğimiz ve somut ilerleme kaydedebileceğimiz lokma boyutunda parçalara bölmek.
Bu yinelemeli yaklaşımın büyük faydaları var: bileşenlerin sürekli entegrasyonunu zorunlu kılar (böylece firmware ve cloud yazılımı sık sık birlikte test edilir) ve geri bildirim alıp gerekirse rota düzeltmek için size bolca fırsat verir. 6 ay boyunca kod yazıp ancak ondan sonra her şeyin çalışıp çalışmadığına bakmaktan çok daha iyidir (ki o da tatsız sürprizlerin reçetesidir). IoT ürün geliştirmemiz için tipik bir sprint döngüsünün nasıl görünebileceğine bakalım:
Sprint Planlama ve User Story’ler
Her sprint bir Sprint Planlama toplantısıyla başlar. Ekip product backlog’a bakar — bu, yapılacak tüm özelliklerin, düzeltmelerin ve görevlerin ana listesidir; başlangıçta Aşama 1’de topladığımız gereksinimlerden türetilmiştir (ve sonraki aşamalarda öğrendiklerimizle beslenmiştir). Planlamada ekip, önceliğe ve ekibin kapasitesine göre backlog’dan sprint’te ele alınacak bir dizi user story veya görev seçer.
User story, bir özelliğin son kullanıcı perspektifinden lokma boyutunda bir tarifidir; genellikle şu şablonu izler: “Bir <kullanıcı tipi> olarak, <bir yetenek> istiyorum ki <fayda> elde edeyim.” Örneğin bir story şöyle olabilir: “Bir kullanıcı olarak, IoT cihazının her 10 dakikada bir cloud’a sıcaklık verisi göndermesini istiyorum ki odamın sıcaklığını uzaktan izleyebileyim.” Bu story’den yola çıkarak ekip, hem cihaz hem cloud tarafında ne yapılması gerektiğini çıkarır: belki cihaz firmware’inde sıcaklık sensörünü okuyup her 600 saniyede bir MQTT mesajı göndermemiz gerekiyor; cloud tarafında bu veriyi alacak bir API veya mesaj işleyicisi ve saklayacak bir yer, belki de göstermek için basit bir arayüz lazım.
Sprint planlamada ekip, seçilen her story’yi kabul kriterlerini netleştirmek için tartışır — yani bu story’nin “bitti” ve çalışıyor olduğunu nereden anlayacağız? Yukarıdaki örnek için kriterler şöyle olabilir: “Sıcaklık sensörlü bir cihaz 10 dakika çalıştığında, en az bir sıcaklık okuması cloud veritabanında ve kullanıcı panosunda görünür olmalı.” Spesifik olmak, ekibin tam olarak neyi inşa edeceğini ve nasıl doğrulayacağını anlamasına yardımcı olur. Soru sorma ve detayları netleştirme zamanı budur. Belirsizlikleri şimdi ütülemek, firmware ve cloud tarafındakilerin farklı varsayımlarla ilerlediğini sonradan keşfetmekten çok daha iyidir (mesela bir geliştirici “aktifken her 10 dakikada bir” sanıyordu, diğeri “her zaman” gibi). Planlama bittiğinde herkes, önümüzdeki sprint’te hangi iş parçasından sorumlu olduğunu bilmeli.
Uygulama ve Continuous Integration (CI)
Şimdi işin uygulama kısmına geliyoruz — o user story’leri hayata geçirecek kodu yazmaya. Geliştiriciler, sprint için belirlenen firmware ve cloud özelliklerini kodlamaya başlar. Buradaki kilit bir en iyi pratik, her yeni özellik geliştirilirken onun otomatik testlerini de yazmaktır. Firmware için bu, host makinede çalışabilen mantık için unit testler yazmayı içerebilir (örneğin bir sensör okumasını parse eden bir fonksiyonunuz veya cihaz için bir state machine’iniz varsa, çoğu zaman bunun için PC tabanlı bir test yazabilirsiniz). Cloud yazılımı içinse genellikle daha kolaydır: fonksiyonlarınız için unit testler, API’leriniz için entegrasyon testleri yazabilirsiniz.
Geliştiriciler kod yazdıkça versiyon kontrolüne (Git) gönderirler. İşte Continuous Integration (CI) kurulumumuzun parladığı yer burası. CI ile, her kod push’landığında veya bir pull request açıldığında, CI sunucusunda otomatik bir build tetiklenir. CI, firmware kodunu derler (gerekirse birden çok konfigürasyonda), sahip olduğunuz unit testleri çalıştırır, hatta belki firmware’i bir simülatöre veya test cihazına yükler. Cloud yazılımını da derler, tüm testlerini çalıştırır, kod stilini kontrol eder (linter’lar), statik analiz araçlarını çalıştırır ve hatta code coverage’ı (kodun ne kadarının testlerce çalıştırıldığını) ölçer. Amaç, sorunları anında yakalamak. Bir geliştirici yanlışlıkla bir testi patlatan bir bug soktuysa, CI sistemi bunu hemen — kod yazıldıktan dakikalar içinde — işaretler ve ekip daha büyük bir soruna dönüşmeden düzeltebilir. Bu hızlı geri bildirim döngüsü hayati. Bir saat önce soktuğunuz bir bug’ı düzeltmek (ne yaptığınızı hâlâ hatırlıyorsunuzdur), geçen aydan kalma (ve o zamana kadar bir sürü başka değişikliğe dolanmış) bir bug’ı düzeltmekten çok daha kolaydır.
Örneğin, bir geliştiricinin sensör okuma kodunu değiştirdiğini ve yanlışlıkla çok yüksek değerler için bir taşma (overflow) bug’ı soktuğunu düşünün. Sensör değerini uç noktaya zorlayan bir unit testiniz varsa, CI bu hatayı yakalar ve ekibi uyarır. Geliştirici de hemen müdahale edebilir. O test olmasaydı, bug bir sıcak hava dalgası gelene ve gerçek sensörler yüksek sıcaklık raporlayana kadar pusuda bekleyebilir, sonra da cihaz sahada çökerdi — eyvah! CI ve testler, titiz ve otomatik olarak bu “eyvah” anlarının çoğunu önler.
Continuous Integration sadece testlerden ibaret değil; ekipler genellikle CI’a statik analiz araçlarını da entegre eder. Statik analiz araçları, kaynak kodunuzu çalıştırmadan potansiyel bug’lar, güvenlik açıkları veya stil ihlalleri için inceler. Örneğin “olası null pointer dereference” veya “kullanılmayan değişkenler” gibi şeyleri ya da diğer code smell’leri yakalayabilirler. Birçok gömülü ekip C/C++ için PC-Lint, Cppcheck veya clang-tidy gibi araçlar kullanır; cloud ekipleri de linter’lar (JavaScript için ESLint, Python için Pylint vb.) ve belki güvenlik tarayıcıları kullanır. CI, bu araçlar ciddi sorunlar bulursa build’i düşürecek şekilde yapılandırılabilir. Bu bir kalite çıtası koyar — örneğin “merge’e izin verilmesi için firmware kodunda 0 yeni uyarı olmalı ve tüm testler geçmeli.” Kulağa katı gelebilir, ama kod tabanını sağlıklı tutar.
Ek olarak CI, code coverage’ı da takip edebilir — kodunuzun yüzde kaçının otomatik testlerce çalıştırıldığını. %100 coverage çoğu zaman gerçekçi olmasa da (ve her kod aynı kolaylıkta test edilemese de), birçok ekip bir hedef belirler (mesela en az %80 coverage) ve takip eder. Yeni bir kod paketi coverage’ı birden %80’den %50’ye düşürdüyse, bu bir kırmızı bayraktır — belki testler atlandı ya da bir sürü test edilmemiş kod eklendi. Bu metrikleri sprint sprint izleyerek, kaliteyi sonradan “yamamaya” çalışmak yerine geliştirme boyunca yüksek kod kalitesini korursunuz.
Özetle, yinelemeli bir bağlamda uygulama sadece “bir yığın kod yazmak” değildir. Kod yaz, o kodu sürekli olarak ortak kod tabanına entegre et, sürekli test et ve sorunları anında düzelt demektir. Sonuç, her zaman yarı-çalışır durumda olan ve her gün biraz daha iyileşen bir yazılım sistemidir.
Artımlı Build’ler ve Sprint Sonu Demoları
Her sprint’in sonunda, tüm sistemin bir öncekine göre somut bir iyileşme olan artımlı bir build’ine sahip olmayı hedeflersiniz. İdealde bu, yeni özellikleri iş başında görmek için gerçekten bir cihazda ve bir test cloud ortamında çalıştırabileceğiniz bir şeydir. Sprint sonunda iyi bir düstur: “her an demo yapmaya hazır ol”. IoT ürün örneğimiz için, belki Sprint 1’in sonunda temel bir uçtan uca akış çalışıyordur: cihaz sahte bir sensör değeri okur (belki sabit bir sayı veya rastgele bir test değeri) ve cloud’a gönderir, cloud alır ve saklar, biz de verinin geçtiğini elle doğrulayabiliriz (sadece veritabanına veya loglara bakmak bile olsa). Bu bile başlı başına dev bir kilometre taşı — cihazdan cloud’a tüm tesisatın çalıştığını kanıtlıyor!
Sprint 2’de sahte değeri donanımdan gelen gerçek bir sensör okumasıyla değiştirebilir ve son okumayı gösteren basit bir web sayfası kurabiliriz. Artık canlı veriyi gerçekten görebiliyoruz. Sprint 3’te belki bir uyarı özelliği ekleriz: sıcaklık bir eşiği aşarsa cihaz veya cloud bir uyarı tetikler (belki bir LED yanıp söner ya da bir e-posta gönderilir). Ve bu böyle sürer. Her sprint’te üstüne yeni işlevsellik katmanları eklersiniz: belki bir sprint uzaktan kontrol desteği ekler (cloud’dan cihaza komut göndermek), bir diğeri geçmiş verilerin şık bir UI grafiğini ekler, vs. Birkaç sprint sonra özellik açısından eksiksiz bir sisteminiz olur. Ama işin can alıcı noktası, her aşamada üzerine ekleme yaptığınız çalışan bir sisteminizin olmasıydı. Bu, bir şeyler ters gittiğinde sorunun büyük olasılıkla en son eklemelerle ilgili olduğunu bilmeniz demek — ki bu da hata ayıklamayı kolaylaştırır. Ayrıca paydaşların (patronunuz, ürün yöneticisi, hatta bir test kullanıcısı) ilerlemeyi görüp süreç boyunca geri bildirim verebilmesi demek.
Düzenli demolar harika bir pratiktir. Her sprint’in sonunda (hatta daha sık, mesela her hafta kısa bir gösterimle) ekibi ve ilgilenen paydaşları toplayıp yenilikleri gösterin. Şu kadar basit olabilir: “Cihaz artık gerçek sensör verisi gönderiyor — bakın, sensörü elimle ısıtıyorum ve bu panodaki sayının gerçek zamanlı yükselişini izleyin!” Bu demolar bir başarı hissi ve ivme yaratır. Ayrıca entegrasyon disiplinini de dayatırlar: gerçekten birlikte çalışmayan bir şeyin demosunu yapamazsınız; bu da ekibi entegrasyon sorunlarını en sona ertelemek yerine yol boyunca ütülemeye iter. Aylarca bir boşlukta çalışmak yerine ürünün adım adım hayat bulduğunu sürekli görmenin ne kadar motive edici olduğuna şaşıracaksınız.
Test ve “Definition of Done” Kriterleri
Geliştirme sırasındaki otomatik testlerden bahsettik, ama testin her sprint’teki ilerleme tanımının ta kendisine nasıl işlendiğini vurgulamakta fayda var. Bir özellik veya user story, yalnızca “kod yazıldı”nın ötesinde belirli kriterleri karşıladığında “bitti” sayılır. Ekiplerin genellikle şu tür maddeler içeren bir Definition of Done (DoD) kontrol listesi vardır:
- Kod Tamam: Özelliğin kodu yazıldı ve hatasız, uyarısız derleniyor.
- Unit Testler: Yeni kodu kapsayan otomatik unit testler yazıldı ve hepsi geçiyor. (Örneğin bir uyarı koşulunu hesaplayan bir fonksiyon yazdıysanız, o fonksiyonun normal ve uç durumları için testler olmalı).
- Entegrasyon Testi Yapıldı: Özellik başka bileşenlerle etkileşiyorsa (ki çoğu eder), daha geniş bir bağlamda test edildi — örneğin cihaz gerçek cloud servisine gerçekten bir mesaj gönderiyor ve uçtan uca aktığını doğruladık. Bu, manuel test veya otomatik entegrasyon testleri içerebilir.
- Regresyon Yok: Mevcut testler (önceki özelliklerden kalanlar) hâlâ geçiyor; yani daha önce çalışan hiçbir şeyi bozmadınız.
- Statik Analiz Temiz: Statik analiz araçları kodda yeni kritik uyarı göstermiyor. Başka bir deyişle, potansiyel bir bellek sızıntısı veya riskli bir yapı sokmadınız.
- Eş Gözden Geçirme (Peer Review): Kod, ekipteki en az bir başka geliştirici tarafından gözden geçirildi ve tüm geri bildirimler ele alındı. Bu, ikinci bir çift gözün mantık hatalarına, okunabilirliğe ve standartlara uygunluğa baktığını garanti eder.
- Dokümantasyon Güncellendi: Bu özellik dokümantasyonda veya diyagramlarda değişiklik gerektiriyorsa, yapıldı. Örneğin yeni bir veri mesajı formatı eklediyseniz, API dokümantasyonunu veya iletişim protokolü spesifikasyonunu buna göre güncellersiniz. Ya da cloud’a yeni bir microservice eklemeye karar verdiyseniz, mimari diyagramını güncellersiniz.
- Paydaş Onayı: İdealde, çalışan özelliğin demosunu product owner’a veya ilgili paydaşa yaptınız ve gereksinimleri (daha önce tanımladığımız kabul kriterlerini) karşıladığında hemfikirsiniz. Bu, story’nin gerçekten ama gerçekten bittiğinin nihai teyididir.
Epey bir liste! İlk bakışta, özellikle bir çaylağa, işaretlenecek çok kutu var gibi görünebilir. Ama bu pratikler sizi ileride bir ton dertten kurtarır. Bir bug’ı veya tasarım kusurunu şimdi, mevcut sprint’te yakalayıp düzeltmek, final testlerine ya da (aman aman) üretime sızmasına izin vermekten çok daha hoştur. Definition of Done özünde şu demektir: “bu özellik yarım yamalak değil; gerçekten entegre edilmiş, test edilmiş ve hazır.” Sisteminizin her parçası yol boyunca bu kriterleri karşıladığında, nihai entegrasyon ve test fazı (Aşama 5) neredeyse heyecansız geçer — çirkin sürprizler olmaz, çünkü baştan beri titiz davrandınız.
Sürekli Dokümantasyon (Haritaları Güncel Tutun)
Mimari fazında (Aşama 4.1) muhtemelen bazı diyagramlar ve dokümanlar oluşturdunuz. Şimdi, geliştirme ilerledikçe, o tasarım dokümanlarını güncel tutmak önemli. Yazılımın evrilme konusunda tuhaf bir huyu vardır — belki bir watchdog fonksiyonu için RTOS’ta yeni bir task’e ihtiyaç olduğunu keşfedersiniz ya da performans için cloud mimarisine bir cache katmanı eklenir. Mimari dokümanlarını güncellemezseniz gerçeklikten koparlar ve sonunda kimse onlara güvenmez. O klasik bayat dokümantasyon sorununu yaşamamak için, dokümanları projenin yaşayan bir parçası olarak görün. Bazı ekipler diyagramlarını PlantUML veya Mermaid gibi araçlarla kod olarak bile yazar. Böylece diyagramın “kaynağı” repository’de kodun yanında yaşar. Mimariyi etkileyen bir değişiklik yapıldığında ekip diyagram kodunu günceller ve görseli yeniden üretir.
Gerçekten şık bir pratik (ekibiniz gönüllüyse), dokümantasyonu CI sürecine de entegre etmektir. Örneğin şöyle bir kontrol koyabilirsiniz: belirli çekirdek dosyalar değiştiyse (bir mesaj şeması veya bir bileşen arayüzü gibi) ama karşılık gelen dokümantasyon dosyası güncellenmediyse, CI size hatırlatır veya build’i düşürür. Geliştiricileri dürter: “Hey, sistemin çalışma şeklini değiştirdin — lütfen diyagramı güncelle ki herkes bilsin.” Abartı gibi gelebilir, ama dokümanları senkron tutmak, ekibe yeni bir geliştirici katıldığında ya da altı ay sonra projeye dönüp işlerin nasıl örgütlendiğini hatırlamaya çalıştığınızda karşılığını verir. Otomatikleştirmeseniz bile, en azından birkaç sprint’te bir diyagramların hâlâ gerçekliği yansıttığından emin olmak için hızlı bir gözden geçirme planlayın.
Sonuç şu: güncel dokümantasyon altın değerindedir. Ekipteki (ya da ekip dışındaki — mesela bir uyumluluk denetçisi veya bir iş ortağı) herkesin dokümanlara bakıp güvenebilmesi demektir. “Wiki’yi okuma, eski o — asıl bilgi Ahmet’in kafasında (ya da kod yorumlarının bir yerinde)” gibi durumlardan kaçınırsınız. Bunun yerine bilgi, siz ilerledikçe yakalanır ve güncellenir — ki bu, olgun ve kalite odaklı bir ekibin alametifarikasıdır.
Düzenli Demolar ve Geri Bildirim Döngüleri
Demolara sprint bağlamında değindik, ama düzenli bir ilerleme gösterme ritmi oturtmanın değerini bir kez daha vurgulayalım. Agile’da bu genellikle Sprint Review toplantısıdır: sprint’in sonunda ekip, inşa ettiklerini paydaşlara gösterir (ürün yöneticileri, diğer ekipler, bazen gerçek kullanıcılar veya yöneticiler bile). Bir IoT projesinde demo kelimenin tam anlamıyla yapılabilir — cihaz ve sistemin birlikte çalıştığını göstererek. Örneğin web panosunu ekrana yansıtır, fiziksel cihazı da masaya koyarsınız. Sonra cihazı tetikler (bir düğmeye basar veya bir koşul yaratırsınız) ve cloud’un gerçek zamanlı güncellendiğini gösterirsiniz. Bu anlar ekip için inanılmaz onaylayıcı, paydaşlar için heyecan vericidir. Projeyi gerçek hissettirir ve herkesin ilgisini canlı tutar. Aynı zamanda geri bildirim alma fırsatıdır: belki bir paydaş, “Harika, ama bir sonraki versiyonda nem verisini de gösterebilir miyiz?” der ya da “Uyarının görünmesi biraz uzun sürdü, hızlandırabilir miyiz?”. Bu tür erken geri bildirimler saf altındır, çünkü ürün finalleşmeden çok önce, değişikliklerin çok daha kolay olduğu dönemde sonraki sprint’lere beslenebilir.
İlk demolarda işlevsellik çok temel diye endişelenmeyin — beklenen bu. Önemli olan yinelemeli olarak iyileşiyor olması. İlk demo sadece bir cihazın bir sunucuyla konuşabildiğini kanıtlıyor olabilir. Sonraki demo basit bir UI gösterir. Daha sonrakiler cilayı ve ek özellikleri gösterir. Bir demoda bir şey beklendiği gibi çalışmadıysa, o da sorun değil — çünkü bir entegrasyon sorununu herkesin önünde keşfettiniz ve artık düzeltmesini önceliklendirebilirsiniz. (Hatta bazı ekiplerin bir lafı vardır: “Erken demo yap, sık demo yap — sık çuvalla, hızlı düzelt!”) Demo ayrıca bir parça disiplin de dayatır: ekibe her sprint işi teslim edilebilir bir duruma bitirme yönünde olumlu bir baskı kurar; entegrasyonu sonraya erteleyip oyalanmak yerine. Cihazın JSON’ının sunucuda doğru parse edilmediğini şimdi öğrenmek, büyük bir final test fazında öğrenmekten çok daha iyidir. Yani demoları sadece bir gösteri olarak değil, geliştirme sürecinin bizzat parçası olarak görün.
Son olarak, o demoları kutlayın! Bir IoT ürünü inşa etmek çok parçalı, zor bir iştir. Bir parçayı daha çalıştırdığınız her seferinde, küçük bir özellik bile olsa, bir an durup takdir edin. Sprint review sonunda çakılan beşlikler veya bir alkış turu gayet yerindedir 🎉. Bu, morali yüksek tutar ve ekip özgüvenini inşa eder. Herkes emeğinin somut sonuçlarını düzenli olarak görebildiğinde enerjinin daha yüksek kaldığını fark edeceksiniz.
Aşama 4’ü özetlersek: Firmware ve cloud yazılımı geliştirme, tamamen yinelemeli ilerleme ve sürekli iyileştirme işidir. Yazılım mimarisine baştan zaman yatırarak ekibinize sağlam bir plan verirsiniz (ve kendinizi bir köşeye kodlamaktan kaçınırsınız). Geliştirme sprint’leri, continuous integration ve güçlü bir definition of done kullanarak, kalitenin her adımda içeriye inşa edilmesini sağlarsınız — bug’lar erken yakalanır ve yazılım her zaman çalışan bir üründen yalnızca bir adım uzaktadır. Aşama 4’ün sonunda (ki muhtemelen birden çok sprint döngüsünü kapsar), gerçek veya temsili donanımda test edilmiş, özellik açısından eksiksiz bir firmware’iniz ve gerçekçi bir ortamda test edilmiş, özellik açısından eksiksiz bir cloud sisteminiz olmalı. Başka bir deyişle, ürününüzün tüm yazılım tarafı yola çıkmaya hazır.
Bu yinelemeli yaklaşım sayesinde, her şeyi sondaki tek bir büyük patlamaya yatırmak yerine sürekli uyum sağlayıp iyileştirebildiniz. Bu, riski dramatik biçimde azaltır ve geliştirme sürecini daha öngörülebilir ve pürüzsüz kılar. Bir de şunu unutmayalım — böylesi çok daha eğlenceli! Uzun bir belirsizlik dönemi yerine, IoT ürününüzün yanıp sönen bir LED’den tamamen bağlantılı, cloud entegre akıllı bir cihaza adım adım hayat buluşunu izlediniz. Kendinizin ve ekibin sırtını sıvazlayın — yazılım tarafındaki ağır iş bitti ve bunu hem titizlikle hem çeviklikle yaptınız. Şimdi bir sonraki aşamaya; bu özenle işlenmiş donanımı ve yazılımı alıp dünyaya fırlatmaya hazırlanacağız!