Bu yazıda Protocol Buffers’ın(Protobuf) ESP32 ve Qt framework ile kullanımına bir örnek vermek istiyorum. Bu örneği hobi olarak uğraştığım bir topraksız tarım projesi üzerinden göstermek istiyorum. Bu örnekte ESP32 ile Qt/QML uygulamasını protobuf ile UDP üzerinden haberleştireceğiz. Burada ESP32, topraksız tarım projesinin gömülü sistem tarafını, Qt/QML ise verileri görüntüleme, kontrol etme vb. işlemleri yapan uygulama tarafını temsil etmektedir. Öncelikle Protocol Buffers nedir buna bir değinelim.
Protobuf Nedir?
Protocol Buffers, Google tarafından geliştirilen bir veri serileştirme metodudur. Protobuf, XML veya JSON gibi metin tabanlı formatların aksine binary format kullanır ve bu da daha düşük boyutlu ve daha hızlı iletişim sağlar. Protobuf’ın temel özelliklerinden biri de bir dil gibi sentaksa ve derleyiciye sahip olmasıdır. Bu yüzden farklı platformlar arasında veri paylaşımını kolaylaştırır ve dil bağımsızlığı sağlar. Protobuf, geniş bir dil desteği sunar ve C++, Java, Python, GO vb. dilleri desteklemektedir. Protobuf daha çok gRPC ile birlikte kullanılsa da kendimize ait bir haberleşme metodu veya paket yapısı ihtiyacı hissettiğimiz herhangi bir uygulamada kullanılabilir. Hatta protobuf’ı kullanmanın diğer alanlara göre daha zor olduğu gömülü sistemlerde bile.
Bir protobuf mesajı en basit hali ile aşağıdaki gibi tanımlanabilir. Bu mesajlar .proto uzantılı bir dosya içerisine yazılır. Örnegin; “person.proto”
1 2 3 4 5 6 7 | syntax = "proto3" message Person { optional string name = 1; optional int32 id = 2; optional string email = 3; } |
Protobuf’ın kendine ait bir sentaksı ve derleyicisi olduğundan bahsetmiştik. Yukarıdaki gibi bir proto mesaj oluşturup aşağıdaki gibi hedef dile göre derleme yapabiliriz.
1 | protoc --cpp_out=. person.proto |
Derleme sonucunda hedef dile göre kütüphane dosyaları oluşturulur. Yukarıdaki örnek için çıktı dosyaları “person_pb.cc” ve “person_pb.h” olacaktır. Bu kütüphaneleri projemize dahil ederek protobuf’ı kullanmaya başlayabiliriz.
Bu örnekte proto mesaj yapısını biraz daha gerçeğe yakın, alt mesajlar içerek şekilde oluşturmak istiyorum. Burada kullanacağım hydroponic_data.proto dosyasının içeriği aşağıdaki gibidir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | syntax = "proto3"; package hydroponic; enum MessageType { MSG_HEART_BEAT = 0; MSG_OK = 1; MSG_ERROR = 2; MSG_DATA = 3; MSG_TIMEOUT = 4; MSG_CMD = 5; } enum CMD { CMD_VALVE_ON = 0; CMD_VALVE_OFF = 1; CMD_PUMP_ON = 2; CMD_PUMP_OFF = 3; CMD_LED_ON = 4; CMD_LED_OFF = 5; } message Hydroponic { MessageType messageType = 1; oneof msg { DataPackage dataPackage = 2; HeartBeat heartBeat = 3; MessageOk messageOk = 4; MessageError messageError = 5; MessageTimeout messageTimeout = 6; Command cmd = 7; } } message DataPackage { uint32 deviceID = 2; string sector = 3; float eConductivity = 4; float ph = 5; float moisture = 6; float temperature = 7; uint32 waterLevel = 8; bool valveState = 9; bool pumpState = 10; bool ledStatus = 11; } message HeartBeat { uint32 elapsedTime = 1; } message MessageOk { string responseMessage = 1; } message MessageError { string errorType = 1; } message MessageTimeout { string timeoutMessage = 1; } message Command { CMD command = 1; } |
Burada bahsetmek isteğim bir diğer konu alt mesaj(submessage) konusudur. Yukarıda verilen .proto uzantılı dosya incelendiğinde burada bir tane üst seviye( top level) mesaj ve bunların altında birden fazla alt mesajlar(submessage) olduğu görülmektedir. Burada “oneof” keywordünün kullanım amacı ise alt mesajlardan tek seferde sadece birinin kullanılacağı anlamına gelmektedir. Yani bir Hydroponic mesajı oluşturulduğunda içinde sadece bir tane alt mesaj olabilir.
Nanopb Nedir?
ESP32 tarafında protocol buffer için Nanopb kütüphanesini kullanacağız. Nanopb, C programlama dilinde kullanılabilen hafif ve verimli bir protobuf kütüphanesidir. Espressif Systems tarafından geliştirilen Nanopb, özellikle mikrodenetleyiciler ve kaynak kısıtlı cihazlar için optimize edilmiştir.
Nanopb kendi derleyicisine sahiptir. Derleme sonrası C kütüphaneleri oluşturur. Nanopb de alt mesaj kullanabilmek için proto dosyasına aşağıdaki satırları ekliyoruz.
1 2 3 | import 'nanopb.proto'; ... option (nanopb_msgopt).submsg_callback = true; |
Nanopb derleyicisi tarafından derlenecek proto dosyasının son hali aşağıdaki gibidir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | syntax = "proto3"; import 'nanopb.proto'; package hydroponic; enum MessageType { MSG_HEART_BEAT = 0; MSG_OK = 1; MSG_ERROR = 2; MSG_DATA = 3; MSG_TIMEOUT = 4; MSG_CMD = 5; } enum CMD { CMD_VALVE_ON = 0; CMD_VALVE_OFF = 1; CMD_PUMP_ON = 2; CMD_PUMP_OFF = 3; CMD_LED_ON = 4; CMD_LED_OFF = 5; } message Hydroponic { MessageType messageType = 1; option (nanopb_msgopt).submsg_callback = true; oneof msg { DataPackage dataPackage = 2; HeartBeat heartBeat = 3; MessageOk messageOk = 4; MessageError messageError = 5; MessageTimeout messageTimeout = 6; Command cmd = 7; } } message DataPackage { uint32 deviceID = 2; string sector = 3; float eConductivity = 4; float ph = 5; float moisture = 6; float temperature = 7; uint32 waterLevel = 8; bool valveState = 9; bool pumpState = 10; bool ledStatus = 11; } message HeartBeat { uint32 elapsedTime = 1; } message MessageOk { string responseMessage = 1; } message MessageError { string errorType = 1; } message MessageTimeout { string timeoutMessage = 1; } message Command { CMD command = 1; } |
Yukarıdaki proto dosyasını nanopb’ye göre derlemek için nanopb’yi indirdiğiniz dizinde bulunan protoc yi kullanmamız gerekmektedir.
1 | pathto\generator-bin\protoc.exe --nanopb_out=. hydroponic_data.proto |
Nanopb’yi ESP32 ile kullanabilmek için yine indirdiğimiz dizinde bulunan aşağıdaki kütüphaneleri ve protoc derleyicisinin ürettiği kütüphaneleri projeye eklememiz gerekmektedir.
Ayrıca nanopb tarafında string ve submessage encode/decode etmek için callback fonksiyonlarının oluşturulması gerekmektedir. Bu fonksiyonları protobuf_callbacks.h kütüphanesinde oluşturuyorum.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | bool write_string(pb_ostream_t *stream, const pb_field_iter_t *field, void * const *arg) { if (!pb_encode_tag_for_field(stream, field)) return false; return pb_encode_string(stream, (uint8_t*)*arg, strlen((char*)*arg)); } bool read_string(pb_istream_t *stream, const pb_field_t *field, void **arg) { uint8_t buffer[128] = {0}; /* We could read block-by-block to avoid the large buffer... */ if (stream->bytes_left > sizeof(buffer) - 1) return false; if (!pb_read(stream, buffer, stream->bytes_left)) return false; /* Print the string, in format comparable with protoc --decode. * Format comes from the arg defined in main(). */ //printf((char*)*arg, buffer); strcpy((char*)*arg, (char*)buffer); return true; } bool msg_callback(pb_istream_t *stream, const pb_field_t *field, void **arg) { // hydroponic_Hydroponic *topmsg = field->message; // ESP_LOGI(TAG,"Message Type: %d" , (int)topmsg->messageType); if (field->tag == hydroponic_Hydroponic_dataPackage_tag) { hydroponic_DataPackage *message = field->pData; message->sector.funcs.decode =& read_string; message->sector.arg = malloc(10*sizeof(char)); } else if (field->tag == hydroponic_Hydroponic_messageOk_tag) { hydroponic_MessageOk *message = field->pData; message->responseMessage.funcs.decode =& read_string; message->responseMessage.arg = malloc(50*sizeof(char)); } else if (field->tag == hydroponic_Hydroponic_messageError_tag) { hydroponic_MessageError *message = field->pData; message->errorType.funcs.decode =& read_string; message->errorType.arg = malloc(50*sizeof(char)); } else if (field->tag == hydroponic_Hydroponic_messageTimeout_tag) { hydroponic_MessageTimeout *message = field->pData; message->timeoutMessage.funcs.decode =& read_string; message->timeoutMessage.arg = malloc(50*sizeof(char)); } return true; } |
Bu aşamadan sonra protobuf’ı ESP32 ile kullanabiliriz.
1 2 3 4 5 6 7 8 9 10 | ... hydroponic_Hydroponic messageToSend = hydroponic_Hydroponic_init_zero; messageToSend.messageType = hydroponic_MessageType_MSG_DATA; messageToSend.which_msg = hydroponic_Hydroponic_dataPackage_tag; // Decide which message will be sent. messageToSend.msg.dataPackage.deviceID = 10; messageToSend.msg.dataPackage.sector.arg = "Sector-1"; messageToSend.msg.dataPackage.sector.funcs.encode =& write_string; messageToSend.msg.dataPackage.temperature = 10.0f; ... |
Kısaca bir mesaj yukarıdaki gibi oluşturulabilir. Mesaj oluşturulduktan sonra aşağıdaki gibi encode fonksiyonu ile serialize edebiliriz.
1 2 3 4 5 | uint8_t buffer[128] = {0}; ... pb_ostream_t ostream = pb_ostream_from_buffer(buffer, sizeof(buffer)); pb_encode(&ostream, hydroponic_Hydroponic_fields, &messageToSend); ... |
Bu aşamadan sonra serialize edilmiş buffer’ı istediğimiz bir haberleşme protokolü üzerinden gönderebiliriz.
1 | sendto(socket, buffer, ostream.bytes_written, 0, (struct sockaddr *)&dest_addr, sizeof(dest_addr)); |
Karşı taraftan gelen protobuf mesajı yine serialize olmuş bir byte dizi seklinde gelecektir. Bu mesajı decode ve callback fonksiyonları ile deserialize edip ilgili verileri çekebiliriz.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | hydroponic_Hydroponic receivedMessage = hydroponic_Hydroponic_init_zero; ... pb_istream_t istream = pb_istream_from_buffer(buffer, len); receivedMessage.cb_msg.funcs.decode = &msg_callback; bool ret = pb_decode(&istream, hydroponic_Hydroponic_fields, receivedMessage); ... if(receivedMessage.which_msg == hydroponic_Hydroponic_dataPackage_tag){ ESP_LOGI(TAG, "Data Package Received."); ESP_LOGI(TAG, "Device ID: %ld", receivedMessage.msg.dataPackage.deviceID); ... } else if(receivedMessage.which_msg == hydroponic_Hydroponic_heartBeat_tag){ ESP_LOGI(TAG, "Heartbeat Package Received."); ESP_LOGI(TAG, "Elapsed time %ld.", receivedMessage.msg.heartBeat.elapsedTime); } ... ... |
Qt ile Kullanımı
Protocol buffers’ı Qt tarafında kullanabilmek için C++ a göre derlememiz gerekmektedir. Protobuf nasıl derleneceği ile ilgili bilgiler için protobuf’ın github sayfasında bulunan dökümantasyonları okuyabilirsiniz. Ayrıca Qt6.6.1 versiyonu ile birlikte protobuf desteği gelmiş. Protobuf derleme sonrasında Qt tabanlı kütüphaneler oluşturulabiliyormuş. En kısa zamanda bunu da denemeyi düşünüyorum. Şimdilik eski usül devam etmek istiyorum.
Derleme sonucu üretilen static kütüphane (.lib vb.) ve header dosyalarını qt projesine dahil etmemiz gerekmektedir. Daha sonra linker’a static kütüphaneyi(libprotobufd.lib) bildirmemiz gerekmekte. Bunu yapmak için .pro uzantılı qt dosyasına aşağıdaki satırları eklememiz gerekmektedir.
1 2 3 4 | LIBS += -L$$PWD/protobuf/ -llibprotobufd INCLUDEPATH += $$PWD/protobuf INCLUDEPATH += $$PWD/protobuf/include DEPENDPATH += $$PWD/protobuf |
Ayrıca protobuf için gerekli olan Run Time Library Debug parametresini aynı dosyaya eklememiz gerekmektedir.
1 | QMAKE_CXXFLAGS_DEBUG += /MTd |
Daha sonra hydroponic.proto uzantılı dosyamızı c++ a göre derleyip gerekli kütüphaneleri üretebiliriz. Burada nanopb için eklediğim satırları siliyorum çünkü protocol buffer derleyicisi olarak githubtan indirip derlediğimiz protobuf’ın derleyicisini kullanacağız. Bu derleyici nanopb yi tanımadığı için .proto dosyası içinde bulunan nanopb ile ilgili kısımlarda hata verecektir.
1 | pathto\protobuf\install\bin\protoc.exe --cpp_out=. hydroponic_data.proto |
Derleme sonucunda üretilen kütüphaneyi yine aynı şekilde qt projemize ekledikten sonra protobuf’ı kullanmaya başlayabiliriz.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | #ifndef PROTOBUFMANAGER_H #define PROTOBUFMANAGER_H #include <QObject> #include <QMap> #include "hydroponic_data.pb.h" #include "udphandler.h" using namespace hydroponic; class ProtobufManager : public QObject { Q_OBJECT public: explicit ProtobufManager(QObject *parent = nullptr); ~ProtobufManager(); enum HydroponicMessageType{ DATA = 0, HEART_BEAT, MESSAGE_OK, MESSAGE_ERROR, MESSAGE_TIMEOUT }; Q_ENUM(HydroponicMessageType) enum HydroponicCMD{ CMD_VALVE_ON = 0, CMD_VALVE_OFF = 1, CMD_PUMP_ON = 2, CMD_PUMP_OFF = 3, CMD_LED_ON = 4, CMD_LED_OFF = 5, }; Q_ENUM(HydroponicCMD) Q_INVOKABLE ProtobufManager::HydroponicMessageType getMessageType(); Q_INVOKABLE int getDeviceId(); Q_INVOKABLE QString getSectorName(); Q_INVOKABLE float getECval(); Q_INVOKABLE float getPh(); Q_INVOKABLE float getMoisture(); // change return type later Q_INVOKABLE float getTemperature(); Q_INVOKABLE int getWaterLevel(); Q_INVOKABLE bool getValveState(); Q_INVOKABLE bool getPumpState(); Q_INVOKABLE bool getLedState(); Q_INVOKABLE void sendCommand(ProtobufManager::HydroponicCMD command); signals: void messageReceived(); public slots: void packageReceived(); private: UdpHandler *udpHandler = nullptr; //Class declaration of protobuf messages Hydroponic hydroponicMessage; // Top level message DataPackage dataMessage; HeartBeat heartBeatMessage; MessageOk messageOk; MessageError messageError; MessageTimeout messageTimeout; HydroponicMessageType messageType; bool parseProtobuf(const QByteArray arr); /* * We cannot access an enum defined inside the Hydroponic class from QML. * Therefore, I want to perform an enum conversion through a look-up table. * */ QMap<HydroponicCMD,hydroponic::CMD> cmdLookUpTable = { {HydroponicCMD::CMD_VALVE_ON, hydroponic::CMD::CMD_VALVE_ON}, {HydroponicCMD::CMD_VALVE_OFF, hydroponic::CMD::CMD_VALVE_OFF}, {HydroponicCMD::CMD_PUMP_ON, hydroponic::CMD::CMD_PUMP_ON}, {HydroponicCMD::CMD_PUMP_OFF, hydroponic::CMD::CMD_PUMP_OFF}, {HydroponicCMD::CMD_LED_ON, hydroponic::CMD::CMD_LED_ON}, {HydroponicCMD::CMD_LED_OFF, hydroponic::CMD::CMD_LED_OFF} }; }; #endif // PROTOBUFMANAGER_H |
Protobuf işlemleri için ProtobufManager isminde bir sınıf oluşturuyorum. Bu sınıf arkaplanda UDP den gelen protobuf mesajları çözüp gerekli verileri elde edecek ve gerektiğinde protobuf mesajını encode edip ESP32 ye gönderecek. Ayrıca bu sınıfı QML tarafında da main.cpp içine aşağıdaki satırı ekleyerek kullanabileceğiz.
1 | qmlRegisterType<ProtobufManager>("com.protobuf", 1, 0, "ProtobufManager"); |
C++ tarafında bir protobuf mesajı oluşturmak için aşağıdaki adımları izleyebiliriz.
Öncelikle bir mesaj sınıfı oluşturulur.
1 | hydroponic::Hydroponic hydroponicMessage; |
Daha sonra hangi alt mesaj gönderilecek ise ilgili mesajın sınıfı oluşturulur.
1 | hydroponic::Command cmdMessage; |
Daha sonra gönderilmek istenen alt mesajın veya var ise top level mesajın alanları doldurulur.
1 | cmdMessage.set_command(hydroponic::CMD::CMD_VALVE_ON); |
Üst seviye mesajın hangi alt seviye mesajı içereceği ayarlarlanır.
1 | hydroponicMessage.set_allocated_cmd(&cmdMessage); |
Bu aşamadan sonra mesaj serialize edilebilir.
1 2 3 4 | QByteArray arr; arr.resize(hydroponicMessage.ByteSizeLong()); //serialize to array hydroponicMessage.SerializeToArray(arr.data(), arr.size()); |
Encode edilmiş veriler bir diziye kaydedildikten sonra istenilen bir haberleşme protokolü ile gönderilebilir. Bu örnekte ESP32 ile haberleşeceğimiz için UDP üzerinden cmd mesajı içeren hydroponic mesajı ESP32’ye gönderilmiştir.
1 | this->udpHandler->sendBytes(arr, this->udpHandler->getSenderAddress(), this->udpHandler->getSenderPort()); |
ESP32 den gelen mesajı decode etmek için ise aşağı adımlar izlenebilir.
Encode edilmiş veriler UDP üzerinden doğru bir şekilde alındıktan sonra aşağıdaki gibi parse fonksiyonları kullanılabilir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | ... auto result = this->hydroponicMessage.ParseFromArray(arr.data(), arr.size()); if(!result){ qInfo() << "Protobuf Parse Error"; return false; } switch (this->hydroponicMessage.messagetype()) { // or we can use hydroponicMessage.has_datapackage() method. case MessageType::MSG_DATA: qInfo() << "data packet received"; this->dataMessage = this->hydroponicMessage.datapackage(); this->messageType = HydroponicMessageType::DATA; break; ... } |
Üst seviye mesaj parse edildikten sonra içinde hangi alt mesaj olduğuna karar verilmesi gerekmektedir. Bunun için protobuf tarafından üretilen sınıfın “has” metodları da kullanılabilir.
1 | hydroponicMessage.has_datapackage() |
Daha sonra ilgili alt mesaj için sınıf oluşturup yukarıdaki gibi altmesajı çekebiliriz.
Bu aşamadan sonra alt mesajın verilerini sınıfın ilgili metodları yardımıyla elde edebiliriz. Hepsi bu kadar. En basit haliyle C++ tarafında kullanımı bu kadar. Daha sonrasında elde edilen verileri kullanabilirsiniz.
QML Tarafında Verilerin Gösterilmesi
Protobuf üzerinden alınan verileri QML ile tasarlanmış bir arayüz üzerinde gösterelim. Yukarıda qmlRegisterType tanımlamasını göstermiştik. Burada tanımlanan isim ile ProtobufManager sınıfını yine aynı isim ile QML tarafında sanki bir componentmiş gibi kullabiliriz.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | ProtobufManager{ id: protobufManager property int xVal: 0 onMessageReceived: { // It will be triggered when a message is received. // check message type switch(protobufManager.getMessageType()){ case ProtobufManager.DATA: // get data sectorText.txt = protobufManager.getSectorName() deviceIdText.txt = protobufManager.getDeviceId() waterLevel.level = protobufManager.getWaterLevel() temperature.temperatureVal = protobufManager.getTemperature() ph.phVal = protobufManager.getPh() humidity.humidityVal = protobufManager.getMoisture() //eConductivity.eConductivityVal = protobufManager.getECval() eConductivity.appendData(xVal++,protobufManager.getECval()) waterPumpOfTank.pumpState = protobufManager.getPumpState() valveOfTank.valveState = protobufManager.getValveState() ledButton.buttonState = protobufManager.getLedState() break; case ProtobufManager.HEART_BEAT: // do stuff break; case ProtobufManager.MESSAGE_OK: // do stuff break; case ProtobufManager.MESSAGE_ERROR: // do stuff break; case ProtobufManager.MESSAGE_TIMEOUT: // do stuff break; default: console.log("Invalid Message Type.") break; } } } |
C++ tarafında signal/slot mekanizması ile mesajın geldiğini QML tarafında algılayıp ilgili verileri çekebiliriz.
1 2 3 4 5 | // protobuf_manager.h ... signals: void messageReceived(); ... |
Protobuf mesajı göndermek için ise istenilen bir durumda aşağıdaki gibi bir mesaj gönderilebilir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | PumpIndicator{ id: waterPumpOfTank width: 300 height: 300 anchors.top: eConductivity.top anchors.left: eConductivity.right anchors.leftMargin: 50 pumpState: true pumpText: "Tank Water Pump" onPumpClicked: { if(pumpState) protobufManager.sendCommand(ProtobufManager.CMD_PUMP_ON) else protobufManager.sendCommand(ProtobufManager.CMD_PUMP_OFF) } } |
ESP tarafından gelen verileri göstermek için geçici olarak aşağıdaki gibi bir arayüz tasarladım. Arayüz biraz basit gelebilir biliyorum ama hala geliştirme aşamasında:)
Sonuç
Protocol Buffers, JSON ve XML in aksine haberleşmede daha düşük bir boyut kullanmaktadır. Daha düşük boyutlu olması sebebi ile haberleşme hızında avantajları olmaktadır. Sadece server-server veya server-client arasındaki haberleşmeden ziyade özel bir paket ihtiyacı duyduğumuz iki gömülü sistem cihazı arasında istenilen haberleşme protokolü(UART, SPI, I2C vb.) üzerinde kullanılabilir. Böylece iki cihazı haberleştirmek için tasarlanması gereken paket yapısı, paketi oluşturma , parse etme vb. işlemlere harcanan zamandan kazanç sağlayabiliriz. Tabii protobuf’ın gömülü sistemlere entegrasyonuna harcanan zamanı saymaz isek 🙂
Yazıda kodların tamamına değinmek mümkün değildi. Bu yüzden olabilidiğince protobuf ile ilgili kısımları göstermeye çalıştım. ESP32 ve Qt kodlarının tamamını incelemek için github hesabımı ziyaret edebilirsiniz -> Github
Çok iyi bir çalışma olmuş emeğinize sağlık
Teşekkürler
Mehmet abi çok güzel yazı olmuş, sayende yeni bilgiler edindik:)
Teşekkür ederim 🙂