Cihaz API'si neden ayrı bir tasarım işidir?
Bir IoT ürününde cihaz API'si, sahadaki donanımla platform arasındaki sözleşmedir: cihazın kim olduğu, hangi veriyi hangi biçimde gönderdiği, hangi komutları kabul ettiği, yapılandırmasını nasıl aldığı ve yazılımını nasıl güncellediği. Web API'lerinden farkı, istemcinin sizin kontrolünüzde olmamasıdır. Bir web uygulamasını sunucuda bir kez güncellemeniz yeter, mobil uygulamada da eski sürümleri zorunlu güncellemeyle devreden çıkarabilirsiniz; sahaya giden cihazların bir kısmı ise yıllarca eski yazılımla çalışır, bazıları hiç güncellenemez. Bu yüzden ilk sürümde verilen kararlar (konu yapısı, mesaj biçimi, kimlik doğrulama) ürünün ömrü boyunca sizinle kalır.Bu yazı, MQTT ağırlıklı bir cihaz API'sinin tasarımını uçtan uca anlatıyor: protokol seçimi, cihaz kimliği ve provizyonlama, konu (topic) yapısı, mesaj biçimi, QoS ve bağlantı yönetimi, komutlar ve durum eşitleme, OTA güncelleme, sürümleme ve işletme. Platform seçimini IoT platformu karşılaştırma yazımızda ayrıca ele aldık.
Protokol seçimi: MQTT, HTTP, CoAP
| Protokol | Bağlantı modeli | Güçlü yanı | Ne zaman |
|---|---|---|---|
| MQTT (3.1.1 / 5.0) | TCP üzerinde sürekli bağlantı, yayınla-abone ol (pub/sub) | Düşük ek yük, buluttan cihaza anında komut, NAT ve güvenlik duvarı arkasından çalışır | Sürekli ya da sık bağlanan cihazlar, ağ geçitleri |
| HTTP(S) | İstek-yanıt; bağlantıyı her zaman cihaz başlatır | Her yerde bilinir, mevcut altyapı ve araçlar kullanılabilir | Seyrek veri gönderen, komut beklemeyen ya da komutu yoklamayla (polling) alabilen cihazlar |
| CoAP (RFC 7252) | UDP üzerinde istek-yanıt | Çok kısıtlı cihaz ve ağlar için küçük başlıklar; LwM2M bunun üzerine kuruludur | Pille çalışan, NB-IoT gibi düşük bant genişlikli bağlantılar |
Cihaz kimliği ve provizyonlama (provisioning)
Sık görülen ve pahalıya patlayan bir hata, tüm cihazlarda aynı kullanıcı adı ve parolanın kullanılmasıdır: bir cihazdan sızan parola tüm filoya kapı açar ve tek bir cihazı ayrıca iptal etmek mümkün olmaz. Kurallar:- Her cihazın kendi kimliği: Tercihen X.509 istemci sertifikasıyla karşılıklı TLS (mTLS). Sertifika kullanılamıyorsa cihaz başına ayrı, iptal edilebilir bir belirteç (token).
- Anahtar cihazdan çıkmaz: Mümkünse özel anahtar cihazda üretilir ve bir güvenli eleman (secure element) ya da işlemcinin korumalı alanında saklanır; üretim hattına düz dosya olarak dağıtılmaz.
- Provizyonlama akışı: Yaygın desen, üretimde cihaza yalnızca kayıt için kullanılabilen ortak bir "talep" (claim) kimliği yüklemek ve cihazın ilk açılışta platformdan kendine özel sertifikayı almasını sağlamaktır. AWS IoT Core'un fleet provisioning özelliği bu deseni hazır sunar. Bu akışta CreateKeysAndCertificate yerine CreateCertificateFromCsr çağrısını kullanın: cihaz yalnızca bir sertifika imzalama isteği (CSR) gönderir ve anahtar cihazda kalır. Talep sertifikasının yetkisini de yalnızca provizyonlama konularıyla sınırlayın.
- İptal ve yenileme: Çalınan ya da hurdaya ayrılan cihaz tek başına iptal edilebilmeli; sertifika süreleri ve yenileme yolu baştan planlanmalıdır. Sunucu süresi dolmuş sertifikayı reddediyorsa, sertifikası sahada dolan ve yenileme yolu olmayan cihaz bağlanamaz ve uzaktan kurtarılamaz.
Konu (topic) yapısı ve erişim kuralları
Konu yapısı, cihaz API'sinin URL şemasıdır ve sonradan değiştirilmesi en zor kısmıdır. Kiracı (tenant), saha ve cihaz kimliğini sabit sırayla taşıyan ve kanalları ayıran bir hiyerarşi işinizi kolaylaştırır:
Kod:
<kiracı>/<saha>/<cihaz-id>/telemetry cihaz -> platform, periyodik ölçümler
<kiracı>/<saha>/<cihaz-id>/event cihaz -> platform, alarm ve olaylar
<kiracı>/<saha>/<cihaz-id>/state cihaz -> platform, son bilinen durum (retained)
<kiracı>/<saha>/<cihaz-id>/status cihaz/broker -> platform, online/offline (retained, Last Will)
<kiracı>/<saha>/<cihaz-id>/cmd/req platform -> cihaz, komut istekleri
<kiracı>/<saha>/<cihaz-id>/cmd/res cihaz -> platform, komut yanıtları
<kiracı>/<saha>/<cihaz-id>/config platform -> cihaz, istenen yapılandırma (retained)
örnek: acme/bursa-1/dev-00123/telemetry
- Erişim kuralı (ACL) cihaz başınadır: Bir cihaz yalnızca kendi telemetry, event, state, status ve cmd/res konularına yazar, yalnızca kendi cmd/req ve config konularını okur. Bunu broker'da, doğrulanmış kimlikten türetilen kurallarla uygulayın (aşağıdaki Mosquitto örneğine bakın); cihazın "doğru konuya yazacağına" güvenmeyin.
- Baştaki eğik çizgi yok: "/acme/..." sıfır uzunlukta bir ilk seviye oluşturur ve "acme/..." ile aynı konu değildir.
- $ ile başlayan konular ayrılmıştır: $SYS broker bilgileri için yaygın biçimde kullanılır; AWS IoT Core $ ile başlayan tüm konuları kendine ayırır ($aws/... gibi), Azure IoT Hub ise $iothub/ önekini kullanır. MQTT spesifikasyonuna göre joker karakterle başlayan abonelikler $ ile başlayan konularla eşleşmez.
- Kişisel veri konuya girmez: Konu adları loglarda ve izleme araçlarında açıkça görünür; müşteri adı, telefon gibi bilgileri konuya değil, gerekiyorsa şifrelenmiş olarak yüke (payload) koyun.
Kod:
# mosquitto.conf: TLS dinleyici, istemci sertifikası zorunlu
listener 8883
cafile /etc/mosquitto/ca/device-ca.crt
certfile /etc/mosquitto/certs/broker.crt
keyfile /etc/mosquitto/certs/broker.key
require_certificate true
use_identity_as_username true
acl_file /etc/mosquitto/acl
# /etc/mosquitto/acl: %u = sertifikadaki CN = cihaz kimliği
pattern write +/+/%u/telemetry
pattern write +/+/%u/event
pattern write +/+/%u/state
pattern write +/+/%u/status
pattern write +/+/%u/cmd/res
pattern read +/+/%u/cmd/req
pattern read +/+/%u/config
Mesaj biçimi: şema, zaman ve sıra
JSON okunaklıdır ve hata ayıklamayı kolaylaştırır. Bant genişliği ya da pil kritikse CBOR veya Protocol Buffers gibi ikili biçimler mesajı genellikle küçültür. Protocol Buffers alan adlarını hiç taşımaz; CBOR alan adlarını koruduğu için kazancı daha sınırlı kalabilir. Kazanç yüke bağlıdır, gerçek mesajlarınızla ölçün. Hangisini seçerseniz seçin, her mesajda şu alanlar olsun:
JSON:
{
"v": 1,
"ts": 1793091600000,
"seq": 4812,
"temp_c": 23.4,
"rh_pct": 41.2,
"bat_mv": 3710
}
- v: Şema sürümü. Platform bu alanla sahadaki her sürümü tanır ve doğru ayrıştırır.
- ts: Ölçümün cihazdaki zamanı (UTC, milisaniye). Platform ayrıca mesajı aldığı zamanı kaydeder; canlı mesajlarda ikisi arasındaki büyük fark saat kaymasına işaret eder.
- seq: Artan sıra numarası. Yeniden gönderilen mesajları ayıklamayı ve kayıp mesajları fark etmeyi sağlar. Yeniden başlatmada sıfırlanıyorsa yanına bir açılış sayacı (boot counter) ekleyin; yoksa yeniden başlatma sonrası mesajlar tekrar sanılır.
- Birim alan adında ya da şemada: temp_c, bat_mv gibi. "temp: 23.4" değerinin Fahrenheit mı Celsius mu olduğunu bir yıl sonra kimse hatırlamaz.
QoS, oturum ve bağlantı yönetimi
- QoS 0 (en fazla bir kez): Arada bir kaybolmasının sorun olmadığı sık telemetri için.
- QoS 1 (en az bir kez): Olaylar, alarmlar ve komutlar için. Mesaj iki kez gelebilir; alıcı tekrarı uygulama düzeyindeki seq ya da komut kimliğiyle ayıklar ve işlemi idempotent (tekrarlandığında sonucu değiştirmeyen) yapar. MQTT'nin paket kimliği (Packet Identifier) bu iş için uygun değildir, çünkü onay işlendikten sonra yeniden kullanılabilir.
- QoS 2 (tam bir kez): Dört paketlik el sıkışma gerektirir ve garanti uçtan uca değil, istemci ile broker arasındaki her aktarım için geçerlidir. AWS IoT Core ve Azure IoT Hub QoS 2'yi hiç desteklemez; QoS 1 artı tekrar ayıklama çoğu zaman daha pratiktir.
- Kalıcı oturum (persistent session): Clean Start = 0 ve sıfırdan büyük bir Session Expiry Interval ile (3.1.1'de CleanSession = 0) bağlanan cihazın aboneliklerini broker saklar, cihaz çevrimdışıyken gelen QoS 1 mesajlarını da kuyrukta tutar. Kuyruktaki komutlara mutlaka bir ömür verin: günler sonra teslim edilen bir "röleyi aç" komutu yarardan çok zarar getirir.
- Saklanan (retained) mesaj: state, status ve config konularında son değer broker'da tutulur ve konuya yeni abone olan herkese hemen gönderilir. Platformlar burada ayrışır: Azure IoT Hub retained mesajları saklamaz; AWS IoT Core ise bunları abonelik anında yalnızca konuya tam adıyla, joker karakter kullanmadan abone olan istemciye gönderir.
- Son vasiyet (Last Will): Cihaz bağlanırken status konusu için retained "offline" vasiyetini tanımlar, bağlandıktan sonra aynı konuya retained "online" yazar. Bağlantı DISCONNECT gönderilmeden koparsa ya da keep alive süresinin 1,5 katı boyunca sessiz kalırsa broker vasiyeti yayınlar. Normal bir DISCONNECT vasiyeti siler; bu yüzden planlı kapanıştan önce cihaz "offline" değerini kendisi yazar.
- Yeniden bağlanma: Broker yeniden başladığında binlerce cihaz aynı saniyede bağlanmaya çalışırsa TLS el sıkışmaları ve kimlik doğrulama yükü broker'ı yeniden düşürebilir. Yeniden bağlanmayı rastgele sapma (jitter) eklenmiş üstel geri çekilmeyle (exponential backoff) yapın: bekleme süresi her başarısız denemede bir üst sınıra kadar ikiye katlanır ve her cihaz bu sürenin rastgele bir kısmını bekler.
Komutlar ve durum eşitleme
Buluttan cihaza iki tür istek vardır ve karıştırılmamalıdır:- Anlık komutlar: "Röleyi aç", "yeniden başlat", "şimdi ölç". İstek, cmd/req konusuna bir komut kimliği ve son geçerlilik zamanıyla gönderilir; cihaz sonucu aynı kimlikle cmd/res konusuna yazar. MQTT 5 bunun için özellikler sunar: yanıt için Response Topic ve Correlation Data, bayatlayan komutların broker kuyruğundan düşmesi için Message Expiry Interval. 3.1.1'de aynı alanları yükün içinde taşıyın. Platform yanıt gelmezse zaman aşımı üretir. Cihaz aynı komut kimliğini yeniden görürse işlemi tekrarlamaz, önceki sonucu yeniden gönderir; süresi geçmiş komutu ise reddeder.
- İstenen durum (desired state): Ölçüm aralığı, eşik değerleri, çalışma modu gibi yapılandırma komut olarak değil, "istenen" ve "bildirilen" (reported) durum olarak tutulur. Cihaz çevrimdışıyken değişen ayar kaybolmaz; cihaz bağlandığında farkı alır, uygular ve bildirilen durumu günceller. Yapılandırmadaki bir sürüm numarası, cihazın ne zaman güncel duruma geldiğini platforma gösterir. AWS IoT'deki Device Shadow ve Azure IoT Hub'daki device twin bu desenin hazır hâlleridir; kendi broker'ınızda retained config (istenen) ve state (bildirilen) konuları aynı işi görür.
Kod:
acme/bursa-1/dev-00123/cmd/req (platform -> cihaz)
{"id":"c-7f3a","op":"relay.set","args":{"ch":1,"on":true},"exp":1793091660000}
acme/bursa-1/dev-00123/cmd/res (cihaz -> platform)
{"id":"c-7f3a","ok":true,"code":0}
OTA güncelleme
Uzaktan yazılım güncellemesi (OTA), cihaz API'sinin en riskli işlemlerinden biridir: hatalı bir güncelleme sahadaki cihazları kullanılamaz hâle getirebilir.- Yazılım imajı (firmware image) imzalanır; cihaz imzayı doğrulamadan yüklemez. İmza tek başına, açığı bilinen eski ama imzalı bir sürüme dönülmesini engellemez; bunun için bir anti-rollback denetimi gerekir. ESP-IDF bunu eFuse'lara yazılan bir güvenlik sürümüyle yapar.
- İki bölümlü (A/B) yapı kullanılır: yeni sürüm boş bölüme yazılır, açılışta sağlık kontrolünden geçmezse cihaz eski sürüme döner. ESP-IDF'te CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE açıkken, esp_ota_mark_app_valid_cancel_rollback() çağrılmadan yeniden başlayan yeni imaj geçersiz sayılır ve önceki sürüm açılır.
- Dağıtım kademelidir: önce iç test cihazları, sonra filonun küçük bir yüzdesi, hata oranı izlendikten sonra tamamı.
- Cihaz ilerlemeyi ve sonucu API üzerinden bildirir (indiriliyor, doğrulandı, kuruldu, geri dönüldü).
- Pil ve bağlantı koşulu kontrol edilir: pil düşükken ya da cihaz pahalı bir hücresel veri bağlantısı üzerindeyken güncelleme ertelenir.
Sürümleme ve uyumluluk
Sahada her zaman birden fazla yazılım sürümü olacaktır. Değişiklikleri mümkün olduğunca ekleme yoluyla yapın: yeni alan eklemek serbesttir, var olan bir alanın adını ya da anlamını değiştirmek yeni bir şema sürümü gerektirir. Bunun işlemesi için platform da cihaz da tanımadığı alanları reddetmek yerine yok saymalıdır. Platform, desteklediği en eski cihaz sürümünü açıkça belirlemeli (ör. son üç sürüm) ve bundan eski cihazlar için bir güncelleme ya da kullanımdan kaldırma planı yapmalıdır. Platforma özgü konu adlarını ve özellikleri cihaz yazılımına dağıtmak yerine tek bir bağlantı katmanında toplamak da bir sürümleme kararıdır: Google'ın Cloud IoT Core hizmetini 2023'te kapatması, tek bir platforma sıkı bağlanmış filoları taşımanın ne kadar zor olabileceğini gösterdi. Standart MQTT ve kendi konu tasarımınız, platform değişikliğini ağ geçidi ya da köprü (bridge) düzeyinde çözmeyi kolaylaştırır; ağ geçidinin rolünü IoT ağ geçidi yazımızda anlattık.Test ve işletme
Gerçek cihaz sayısına ulaşmadan önce bir cihaz simülatörüyle binlerce sanal istemci bağlayıp yük testi yapın; bağlantı sayısını, saniyedeki mesaj sayısını, yeniden bağlanma fırtınasındaki davranışı ve broker'ın bellek kullanımını ölçün. Üretimde şunları izleyin: bağlı cihaz sayısı, cihaz başına mesaj hızı, reddedilen bağlantılar (süresi dolmuş ya da iptal edilmiş sertifikalar dahil), ACL ihlalleri, komutların yanıt süresi ve OTA başarı oranı. Bir cihazın beklenenden çok daha fazla mesaj göndermesi genellikle bir yazılım hatasına (ör. sınırsız bir yeniden deneme döngüsüne) işaret eder; cihaz başına hız sınırı koyun.Sık yapılan hatalar
- Tüm filoda ortak kullanıcı adı ve parola.
- ACL'siz bir broker ya da cihazın kendi seçtiği istemci kimliğine (client ID) bağlı ACL'ler.
- Birimsiz, sürümsüz, zaman damgasız mesajlar.
- Yapılandırmayı anlık komutla göndermek: cihaz o an çevrimdışıysa ayar kaybolur.
- İmzasız ve geri dönüşü olmayan OTA.
- Jitter'sız yeniden bağlanma: broker'ın her yeniden başlatılışı bir kesintiye dönüşür.
Sık sorulan sorular
MQTT mi, HTTP mi?Cihaz buluttan komut alacaksa ya da sık veri gönderiyorsa MQTT; günde birkaç kez veri gönderen ve komut beklemeyen bir cihaz için HTTP yeterli ve daha basittir. Çok kısıtlı, pille çalışan cihazlarda CoAP ve LwM2M değerlendirilmelidir.
Kendi broker'ımı mı kurmalıyım, bulut hizmeti mi kullanmalıyım?
Cihaz sayısı, verinin nerede tutulması gerektiği gibi uyum gereksinimleri, ekip yetkinliği ve maliyet belirler. Yönetilen hizmetler işletme yükünü üstlenir; Mosquitto, EMQX ya da HiveMQ gibi broker'ları kendiniz işletmek ise veri ve maliyet üzerinde daha fazla denetim verir. Yönetilen ve kendi işlettiğiniz seçenekleri platform yazımızda karşılaştırdık.
JSON mı, ikili biçim mi?
Başlangıçta ve hata ayıklamada JSON. Hücresel veri maliyeti ya da pil ömrü kritikse CBOR veya Protocol Buffers; şema sürümü alanını her iki durumda da koruyun.
Cihaz saatine güvenebilir miyim?
Tek başına hayır. Cihaz saatini NTP ya da ağ zamanıyla eşitleyin, yine de platformun mesajı aldığı zamanı ayrıca kaydedin ve aradaki farkı izleyin.
Sertifika süresi ne olmalı?
Cihazın sahadaki ömrüne ve yenileme yolunun güvenilirliğine bağlıdır. Yenileme otomatik ve test edilmişse kısa süreler güvenliği artırır; yenileme yolu yoksa uzun süreli sertifika bilinçli bir risk kabulüdür ve belgelenmelidir.
Sonuç ve sonraki adımlar
Sahadaki en eski cihazı düşünerek tasarlayın: kimlik, konu yapısı ve mesaj şeması baştan doğru kurulursa platform ya da broker değişse bile filo yönetilebilir kalır.- Cihaz başına kimlik ve provizyonlama akışını ilk prototipten önce tasarlayın.
- Konu hiyerarşisini ve cihaz başına ACL kurallarını yazıya dökün; platformunuzun konu ve QoS kısıtlarını kontrol edin.
- Mesaj şemasına sürüm, zaman damgası ve sıra numarası ekleyin.
- Yapılandırmayı istenen/bildirilen durum olarak, işlemleri komut olarak ayırın.
- İmzalı, A/B bölümlü ve kademeli dağıtılan bir OTA akışı kurun; yük testini gerçek filo boyutunun üzerinde yapın.
Why a device API is its own design job
In an IoT product, the device API is the contract between hardware in the field and the platform: who the device is, what data it sends and in which format, which commands it accepts, how it receives its configuration and how its firmware is updated. The difference from a web API is that you do not control the client. A web app is updated once on the server, and a mobile app can force old versions to upgrade; some of the devices you ship will run old firmware for years, and some can never be updated at all. Decisions made in the first version (topic structure, message format, authentication) therefore stay with you for the life of the product.This article walks through the design of an MQTT-centred device API end to end: choosing a protocol, device identity and provisioning, topic structure, message format, QoS and connection handling, commands and state sync, OTA updates, versioning and operations. Choosing a platform is covered separately in our IoT platform comparison.
Choosing a protocol: MQTT, HTTP, CoAP
| Protocol | Connection model | Strength | When |
|---|---|---|---|
| MQTT (3.1.1 / 5.0) | Persistent TCP connection, publish-subscribe | Low overhead, instant cloud-to-device commands, works from behind NAT and firewalls | Devices that stay connected or connect often, gateways |
| HTTP(S) | Request-response; the device always opens the connection | Known everywhere, existing infrastructure and tooling | Devices that send data rarely and expect no commands, or can poll for them |
| CoAP (RFC 7252) | Request-response over UDP | Small headers for very constrained devices and networks; LwM2M builds on it | Battery-powered devices on low-bandwidth links such as NB-IoT |
Device identity and provisioning
A common and costly mistake is one username and password shared by every device: a password leaked from one device opens the whole fleet, and you cannot revoke a single device on its own. The rules:- Every device has its own identity: preferably mutual TLS (mTLS) with an X.509 client certificate. Where certificates are not an option, a separate, revocable token per device.
- The key never leaves the device: where possible the private key is generated on the device and kept in a secure element or a protected area of the processor, rather than handed to the production line as a plain file.
- A provisioning flow: a common pattern is to load a shared "claim" identity at the factory that can only be used for registration, and have the device obtain its own certificate from the platform on first boot. AWS IoT Core's fleet provisioning offers this ready-made. In that flow, use CreateCertificateFromCsr rather than CreateKeysAndCertificate: the device sends only a certificate signing request (CSR), so the key stays on it. Limit the claim certificate's policy to the provisioning topics.
- Revocation and renewal: a stolen or scrapped device must be revocable on its own, and certificate lifetimes and the renewal path must be planned from the start. If the server rejects expired certificates, a device whose certificate lapses in the field with no renewal path can no longer connect and cannot be recovered remotely.
Topic structure and access rules
The topic structure is the URL scheme of your device API, and it is the hardest part to change later. A hierarchy that carries tenant, site and device ID in a fixed order and separates the channels makes life easier:
Kod:
<tenant>/<site>/<device-id>/telemetry device -> platform, periodic measurements
<tenant>/<site>/<device-id>/event device -> platform, alarms and events
<tenant>/<site>/<device-id>/state device -> platform, last known state (retained)
<tenant>/<site>/<device-id>/status device/broker -> platform, online/offline (retained, Last Will)
<tenant>/<site>/<device-id>/cmd/req platform -> device, command requests
<tenant>/<site>/<device-id>/cmd/res device -> platform, command responses
<tenant>/<site>/<device-id>/config platform -> device, desired configuration (retained)
example: acme/bursa-1/dev-00123/telemetry
- Access rules (ACLs) are per device: a device writes only to its own telemetry, event, state, status and cmd/res topics and reads only its own cmd/req and config. Enforce this in the broker with rules derived from the authenticated identity (see the Mosquitto example below); do not trust the device to "write to the right topic".
- No leading slash: "/acme/..." creates a zero-length first level and is a different topic from "acme/...".
- Topics starting with $ are reserved: $SYS is widely used for broker information, AWS IoT Core reserves every topic that starts with $ (such as $aws/...), and Azure IoT Hub uses $iothub/. Under the MQTT specification, subscriptions that start with a wildcard do not match $ topics.
- No personal data in topics: topic names show up in plain text in logs and monitoring tools; customer names or phone numbers belong in the payload, encrypted if necessary, never in the topic.
Kod:
# mosquitto.conf: TLS listener, client certificate required
listener 8883
cafile /etc/mosquitto/ca/device-ca.crt
certfile /etc/mosquitto/certs/broker.crt
keyfile /etc/mosquitto/certs/broker.key
require_certificate true
use_identity_as_username true
acl_file /etc/mosquitto/acl
# /etc/mosquitto/acl: %u = certificate CN = device ID
pattern write +/+/%u/telemetry
pattern write +/+/%u/event
pattern write +/+/%u/state
pattern write +/+/%u/status
pattern write +/+/%u/cmd/res
pattern read +/+/%u/cmd/req
pattern read +/+/%u/config
Message format: schema, time and sequence
JSON is readable and easy to debug. When bandwidth or battery is critical, binary formats such as CBOR or Protocol Buffers usually make messages smaller. Protocol Buffers drops field names entirely; CBOR keeps them, so its saving can be more modest. The saving depends on the payload, so measure it on real messages. Whichever you choose, every message should carry these fields:
JSON:
{
"v": 1,
"ts": 1793091600000,
"seq": 4812,
"temp_c": 23.4,
"rh_pct": 41.2,
"bat_mv": 3710
}
- v: the schema version, so the platform can recognise and parse every version in the field.
- ts: the measurement time on the device (UTC, milliseconds). The platform also records its own receive time; on live messages, a large gap between the two points to clock drift.
- seq: an increasing sequence number, for discarding resent messages and spotting lost ones. If it resets on reboot, pair it with a boot counter so that messages after a restart do not look like duplicates.
- Units in the field name or the schema: temp_c, bat_mv and so on. A year later nobody will remember whether "temp: 23.4" was Fahrenheit or Celsius.
QoS, sessions and connection handling
- QoS 0 (at most once): for frequent telemetry where an occasional loss does not matter.
- QoS 1 (at least once): for events, alarms and commands. A message can arrive twice, so the receiver drops duplicates by an application-level seq or command ID and processes them idempotently. The MQTT Packet Identifier will not do, because it can be reused once the acknowledgement has been processed.
- QoS 2 (exactly once): costs a four-packet handshake and covers each client-broker hop, not the path end to end. AWS IoT Core and Azure IoT Hub do not support it at all; QoS 1 plus duplicate filtering is usually more practical.
- Persistent sessions: with Clean Start = 0 and a Session Expiry Interval above zero (CleanSession = 0 in 3.1.1), the broker keeps the device's subscriptions and queues QoS 1 messages while it is offline. Give queued commands an expiry: a "switch the relay on" delivered days later does more harm than good.
- Retained messages: on state, status and config topics the broker keeps the latest value and hands it to each new subscriber. Platforms differ: Azure IoT Hub does not store retained messages, and AWS IoT Core delivers them at subscribe time only when the subscription names the exact topic, without wildcards.
- Last Will: on connect, the device registers a retained "offline" will on its status topic, then publishes a retained "online" there. If the connection drops without a DISCONNECT, or stays silent for one and a half times the keep-alive interval, the broker publishes the will. A normal DISCONNECT discards the will, so before a planned shutdown the device writes "offline" itself.
- Reconnecting: if thousands of devices reconnect in the same second after a broker restart, the burst of TLS handshakes and authentication can knock the broker over again. Reconnect with exponential backoff and random jitter: double the wait after each failure up to a cap, and let each device wait a random part of it.
Commands and state sync
There are two kinds of cloud-to-device requests, and they must not be confused:- Immediate commands: "switch the relay on", "reboot", "measure now". The request goes to cmd/req with a command ID and an expiry time, and the device answers on cmd/res with the same ID. MQTT 5 has properties for this: Response Topic and Correlation Data for the reply, Message Expiry Interval so the broker drops stale queued commands; on 3.1.1, carry the same fields in the payload. The platform raises a timeout when no reply arrives. A device that sees a command ID again resends its earlier result instead of acting twice, and it rejects commands past their expiry.
- Desired state: configuration such as the measurement interval, thresholds or operating mode is kept as "desired" and "reported" state rather than sent as commands. A setting changed while the device was offline is not lost: when the device connects it receives the difference, applies it and updates the reported state. A version number on the configuration tells the platform when the device has caught up. Device Shadow in AWS IoT and device twins in Azure IoT Hub are ready-made versions of this pattern; on your own broker, a retained config topic (desired) and the state topic (reported) do the same job.
Kod:
acme/bursa-1/dev-00123/cmd/req (platform -> device)
{"id":"c-7f3a","op":"relay.set","args":{"ch":1,"on":true},"exp":1793091660000}
acme/bursa-1/dev-00123/cmd/res (device -> platform)
{"id":"c-7f3a","ok":true,"code":0}
OTA updates
Over-the-air firmware updates (OTA) are among the riskiest operations in a device API: a faulty update can leave devices in the field unusable.- The firmware image is signed, and the device verifies the signature before installing. A signature alone does not prevent a downgrade to an older signed build with known flaws; that needs an anti-rollback check, which ESP-IDF implements with a security version stored in eFuses.
- An A/B partition layout is used: the new version goes to the spare partition, and if it fails its health check at boot the device falls back to the old one. In ESP-IDF, with CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE set, an image that reboots before calling esp_ota_mark_app_valid_cancel_rollback() is abandoned and the previous one boots.
- Rollout is staged: internal test devices first, then a small percentage of the fleet, and the rest only after the failure rate has been watched.
- The device reports progress and outcome through the API (downloading, verified, installed, rolled back).
- Battery and connectivity are checked: the update is postponed when the battery is low or the device is on an expensive cellular data connection.
Versioning and compatibility
There will always be more than one firmware version in the field. Make changes additive wherever possible: adding a field is free, but renaming a field or changing its meaning needs a new schema version. For that to work, both platform and device must ignore unknown fields rather than reject them. The platform should state the oldest device version it supports (the last three releases, for example) and plan an update or retirement path for anything older. Keeping platform-specific topic names and features in one connectivity layer, instead of scattering them through the firmware, is a versioning decision too: Google's shutdown of Cloud IoT Core in 2023 showed how hard it can be to move a fleet tied tightly to one platform. Standard MQTT and your own topic design make it easier to handle a platform change at the gateway or bridge level; we describe the role of the gateway in our IoT gateway article.Testing and operations
Before you reach your real fleet size, load test with a device simulator that connects thousands of virtual clients, and measure connection counts, messages per second, behaviour during a reconnect storm and the broker's memory use. In production, watch the number of connected devices, the message rate per device, rejected connections (including expired or revoked certificates), ACL violations, command response times and the OTA success rate. A device sending far more messages than expected usually points to a firmware bug such as an unbounded retry loop, so set a per-device rate limit.Common mistakes
- One shared username and password for the whole fleet.
- A broker without ACLs, or ACLs keyed to the client ID that the device picks itself.
- Messages without units, versions or timestamps.
- Sending configuration as an immediate command: if the device is offline at that moment, the setting is lost.
- Unsigned OTA with no way back.
- Reconnecting without jitter: every broker restart turns into an outage.
Frequently asked questions
MQTT or HTTP?MQTT if the device receives commands from the cloud or sends data often; HTTP is enough, and simpler, for a device that sends data a few times a day and expects no commands. For very constrained battery-powered devices, consider CoAP and LwM2M.
Should I run my own broker or use a cloud service?
Device count, compliance requirements such as where data must be stored, team skills and cost decide. Managed services take on the operational load; running brokers such as Mosquitto, EMQX or HiveMQ yourself gives more control over data and cost. Our platform article compares managed and self-hosted options.
JSON or a binary format?
JSON to start with and for debugging. CBOR or Protocol Buffers when cellular data costs or battery life are critical; keep the schema version field either way.
Can I trust the device clock?
Not on its own. Sync the device clock via NTP or network time, but still record the platform's receive time and watch the gap between the two.
How long should device certificates last?
It depends on the device's life in the field and how reliable the renewal path is. If renewal is automatic and tested, shorter lifetimes improve security; if there is no renewal path, a long-lived certificate is a deliberate risk acceptance and should be documented as such.
Conclusion and next steps
Design for the oldest device in the field: if identity, topic structure and message schema are right from the start, the fleet stays manageable even when the platform or broker changes.- Design per-device identity and the provisioning flow before the first prototype.
- Write down the topic hierarchy and the per-device ACL rules, and check your platform's topic and QoS limits.
- Add a version, a timestamp and a sequence number to the message schema.
- Separate configuration (desired/reported state) from actions (commands).
- Build a signed, A/B-partitioned OTA flow with staged rollout, and load test above your real fleet size.
Son düzenleyen: