[{"data":1,"prerenderedAt":729},["ShallowReactive",2],{"blog-post-updating-firmware-on-a-fleet-without-bricking-it":3,"blog-posts-pl":664},{"original":4,"pl":386},{"id":5,"title":6,"author":7,"body":8,"date":369,"description":370,"extension":371,"lang":372,"meta":373,"navigation":149,"path":374,"readingTime":375,"seo":376,"sitemap":377,"stem":378,"tags":379,"updated":384,"__hash__":385},"blog/blog/updating-firmware-on-a-fleet-without-bricking-it.md","Updating firmware on fifty thousand ESP32s without bricking any","Bartosz Szczepanik",{"type":9,"value":10,"toc":360},"minimark",[11,15,18,23,39,46,49,53,60,63,289,292,296,299,302,306,309,312,315,318,322,325,336,339,343,346,350,353,356],[12,13,14],"p",{},"The first over-the-air update on a new product usually goes fine. It goes to a handful of boards on a desk, all of them on good Wi-Fi, all of them a few metres from the person who wrote the code. The update that hurts comes a year later, when the fleet has grown to tens of thousands of units sitting in basements, on rooftops and behind metal cabinets, and a small change to the network stack means a few percent of them never come back online. Nobody is going to drive out to a thousand sites to reflash a board by hand, so the whole update path has to assume that something will go wrong and still leave every device in a state it can recover from.",[12,16,17],{},"This is how I approach it on ESP32 based devices. Most of it carries over to other microcontrollers with a little translation.",[19,20,22],"h2",{"id":21},"two-slots-and-a-way-back","Two slots and a way back",[12,24,25,26,30,31,34,35,38],{},"The foundation is the partition table. ESP-IDF supports two application slots, usually called ",[27,28,29],"code",{},"ota_0"," and ",[27,32,33],{},"ota_1",", plus a small ",[27,36,37],{},"otadata"," partition that tells the bootloader which slot to run. The device always runs from one slot and downloads the new image into the other. If the download fails halfway, nothing has changed and the device carries on with the old firmware. Only once the new image is fully written and its checksum verified does the device switch the boot pointer and restart.",[12,40,41,42,45],{},"That covers a broken download. It does nothing for an image that downloads perfectly and then fails to work, which is the case that actually bricks devices in the field. For that you want the bootloader's rollback feature, enabled with ",[27,43,44],{},"CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE",". With it switched on, a freshly installed image boots in a pending state. If the application confirms that it is healthy, the image becomes the new normal. If the device restarts before that confirmation happens, the bootloader treats the new image as failed and goes back to the previous slot on its own.",[12,47,48],{},"The whole design then hinges on what the application counts as healthy.",[19,50,52],{"id":51},"deciding-when-an-update-has-worked","Deciding when an update has worked",[12,54,55,56,59],{},"I have seen firmware that marks itself valid on the first line of ",[27,57,58],{},"app_main",". That protects against an image that crashes before it gets that far and against very little else. A device that boots happily and then can never reach the broker again is exactly as lost as one that crashes, and it is also the more likely failure after an update that touched Wi-Fi, TLS certificates or the modem driver.",[12,61,62],{},"What I prefer is to confirm the image only after the device has done the one thing it exists for. It connects to the network, completes the TLS handshake, reaches the MQTT broker and publishes a message that the backend acknowledges. Until then the image stays pending, and a task watchdog or a plain timer restarts the device if it takes too long, which sends it back to the old firmware.",[64,65,70],"pre",{"className":66,"code":67,"language":68,"meta":69,"style":69},"language-c shiki shiki-themes arch-amber arch-amber-light","static void confirm_update_if_pending(void)\n{\n    const esp_partition_t *running = esp_ota_get_running_partition();\n    esp_ota_img_states_t state;\n\n    if (esp_ota_get_state_partition(running, &state) != ESP_OK ||\n        state != ESP_OTA_IMG_PENDING_VERIFY) {\n        return;\n    }\n\n    // Called only after the first telemetry message has been acknowledged\n    // by the backend, so the new image has proven it can do its job.\n    esp_ota_mark_app_valid_cancel_rollback();\n    ESP_LOGI(TAG, \"update confirmed on %s\", running->label);\n}\n","c","",[27,71,72,99,105,132,144,151,189,206,214,220,225,232,238,246,283],{"__ignoreMap":69},[73,74,77,81,85,89,93,96],"span",{"class":75,"line":76},"line",1,[73,78,80],{"class":79},"s-Rrv","static",[73,82,84],{"class":83},"sSq2B"," void",[73,86,88],{"class":87},"swbo2"," confirm_update_if_pending",[73,90,92],{"class":91},"sfVkF","(",[73,94,95],{"class":83},"void",[73,97,98],{"class":91},")\n",[73,100,102],{"class":75,"line":101},2,[73,103,104],{"class":91},"{\n",[73,106,108,111,115,119,123,126,129],{"class":75,"line":107},3,[73,109,110],{"class":79},"    const",[73,112,114],{"class":113},"sotT8"," esp_partition_t",[73,116,118],{"class":117},"sxJ4f"," *",[73,120,122],{"class":121},"sile1","running ",[73,124,125],{"class":117},"=",[73,127,128],{"class":87}," esp_ota_get_running_partition",[73,130,131],{"class":91},"();\n",[73,133,135,138,141],{"class":75,"line":134},4,[73,136,137],{"class":113},"    esp_ota_img_states_t",[73,139,140],{"class":121}," state",[73,142,143],{"class":91},";\n",[73,145,147],{"class":75,"line":146},5,[73,148,150],{"emptyLinePlaceholder":149},true,"\n",[73,152,154,157,160,163,165,168,171,174,177,180,183,186],{"class":75,"line":153},6,[73,155,156],{"class":79},"    if",[73,158,159],{"class":91}," (",[73,161,162],{"class":87},"esp_ota_get_state_partition",[73,164,92],{"class":91},[73,166,167],{"class":87},"running",[73,169,170],{"class":91},",",[73,172,173],{"class":117}," &",[73,175,176],{"class":87},"state",[73,178,179],{"class":91},")",[73,181,182],{"class":117}," !=",[73,184,185],{"class":121}," ESP_OK ",[73,187,188],{"class":117},"||\n",[73,190,192,195,198,201,203],{"class":75,"line":191},7,[73,193,194],{"class":121},"        state ",[73,196,197],{"class":117},"!=",[73,199,200],{"class":121}," ESP_OTA_IMG_PENDING_VERIFY",[73,202,179],{"class":91},[73,204,205],{"class":91}," {\n",[73,207,209,212],{"class":75,"line":208},8,[73,210,211],{"class":79},"        return",[73,213,143],{"class":91},[73,215,217],{"class":75,"line":216},9,[73,218,219],{"class":91},"    }\n",[73,221,223],{"class":75,"line":222},10,[73,224,150],{"emptyLinePlaceholder":149},[73,226,228],{"class":75,"line":227},11,[73,229,231],{"class":230},"srOGa","    // Called only after the first telemetry message has been acknowledged\n",[73,233,235],{"class":75,"line":234},12,[73,236,237],{"class":230},"    // by the backend, so the new image has proven it can do its job.\n",[73,239,241,244],{"class":75,"line":240},13,[73,242,243],{"class":87},"    esp_ota_mark_app_valid_cancel_rollback",[73,245,131],{"class":91},[73,247,249,252,254,257,259,262,266,269,271,274,277,280],{"class":75,"line":248},14,[73,250,251],{"class":87},"    ESP_LOGI",[73,253,92],{"class":91},[73,255,256],{"class":87},"TAG",[73,258,170],{"class":91},[73,260,261],{"class":91}," \"",[73,263,265],{"class":264},"sP85D","update confirmed on %s",[73,267,268],{"class":91},"\"",[73,270,170],{"class":91},[73,272,273],{"class":87}," running",[73,275,276],{"class":91},"->",[73,278,279],{"class":87},"label",[73,281,282],{"class":91},");\n",[73,284,286],{"class":75,"line":285},15,[73,287,288],{"class":91},"}\n",[12,290,291],{},"The timeout deserves some thought. A device on a weak cellular link can take a couple of minutes to register on the network, so a thirty second limit would roll back perfectly good updates. I usually give it several minutes and let the device report which version it came back on, so the backend can tell a rollback apart from a device that simply went quiet.",[19,293,295],{"id":294},"only-run-what-you-signed","Only run what you signed",[12,297,298],{},"Anything that can install new code over the air is also an attack surface, and a fleet that accepts unsigned images is one leaked credential away from running someone else's firmware. On the ESP32 family the answer is Secure Boot v2, which checks the signature of the bootloader and the application on every boot and of every image that arrives over the air, against a public key whose digest is burned into eFuses. On the original ESP32 it needs chip revision 3.0 or newer, which is worth checking before the first production order. Flash encryption goes alongside it so the firmware cannot simply be read off the chip.",[12,300,301],{},"Both of these are one-way decisions made at manufacturing time, which is why I like to settle them before the first production batch. Retrofitting secure boot onto devices that are already deployed ranges from awkward to impossible. The same goes for anti-rollback. ESP-IDF can store a security version in eFuses and refuse to boot anything older, which stops an attacker from installing an old image with a known vulnerability. It is worth enabling, and also worth using carefully, since every bump permanently burns a bit. On the ESP32 the field holds 32 of them, so I raise the security version only for releases that fix a real vulnerability.",[19,303,305],{"id":304},"rolling-out-in-waves","Rolling out in waves",[12,307,308],{},"Even with rollback in place, I never send a new version to the whole fleet at once. A bad image that rolls back cleanly still costs every device a download, a restart and a few minutes offline, and some failures only show up hours later, after a memory leak has had time to grow or after the device has gone through a night of low temperatures.",[12,310,311],{},"The pattern that has served me well starts with a few dozen internal devices, moves to around one percent of the real fleet, then ten percent, then everyone. Each wave waits long enough to cover at least one full daily cycle of whatever the devices do. The backend watches a small set of numbers for the updated group against the rest of the fleet, mainly how many came back on the new version, how many rolled back, reconnect rates, free heap and the rate of crash reports. If any of them drift, the rollout stops by itself.",[12,313,314],{},"AWS IoT Jobs has most of this built in, with rollout rates that can grow over time and abort rules based on the share of devices that fail or time out. If you run your own broker, a small service that walks a device list in batches and checks a few metrics between batches does the same job, and it is not much code.",[12,316,317],{},"I also like to pick the waves deliberately. The first real group should include the oldest hardware revisions, the weakest connections and the sites in the coldest places, because those are where problems show up first. A canary group made only of healthy devices on good networks tells you very little.",[19,319,321],{"id":320},"the-cost-of-a-megabyte","The cost of a megabyte",[12,323,324],{},"On Wi-Fi nobody thinks about image size. On cellular it adds up quickly. Once an ESP32 application has a TLS stack, an MQTT client and the product logic in it, an image of a megabyte or more is normal. At one and a half megabytes, sending it to fifty thousand devices moves roughly seventy five gigabytes of data. On an IoT data plan priced per megabyte, a single release can cost more than a month of normal telemetry, and the fleet will still need several releases a year.",[12,326,327,328,331,332,335],{},"A few habits help here. Keeping the image lean pays off every time you update. Resumable downloads mean a device that loses its connection at ninety percent does not start again from zero. Recent ESP-IDF releases support this in ",[27,329,330],{},"esp_https_ota"," through the ",[27,333,334],{},"ota_resumption"," option, which picks up a failed download with an HTTP range request. Delta updates, where the device downloads only the difference between its current image and the new one, can shrink a small change to a fraction of the full size. They add complexity on both the build side and the device, and I would only reach for them once the data bill makes a clear case.",[12,337,338],{},"Battery powered devices need one more rule, which is to never start an update below a comfortable charge level. Writing flash and holding a modem connection for a few minutes draws far more current than the device's normal routine, and a battery that dies halfway through leaves the device on the old slot at best.",[19,340,342],{"id":341},"what-the-backend-needs-to-know","What the backend needs to know",[12,344,345],{},"Every device should report its running firmware version, the slot it booted from and whether the last boot followed a rollback. With that in place, the fleet's version spread becomes a chart you can look at, and questions like how many devices are still on the version from March get an answer in seconds. I also keep a record of which release each device was offered, when it downloaded it and when it confirmed it. When something goes wrong, that history is usually the fastest way to see a pattern, such as every failure sharing a hardware revision or a mobile operator.",[19,347,349],{"id":348},"where-i-would-start","Where I would start",[12,351,352],{},"If I had to pick the pieces that matter most for a new product, I would put the two slot layout with rollback, confirmation tied to a real round trip to the backend, and signed images at the top, because those are hard or impossible to add later. Staged rollouts, resumable downloads and fleet version reporting can all be built on top once the devices are out there.",[12,354,355],{},"If you are working out an update strategy for a fleet and want to talk it through, I am always happy to compare notes.",[357,358,359],"style",{},"html pre.shiki code .s-Rrv, html code.shiki .s-Rrv{--shiki-default:#F59E0B;--shiki-default-font-weight:bold;--shiki-light:#B45309;--shiki-light-font-weight:bold}html pre.shiki code .sSq2B, html code.shiki .sSq2B{--shiki-default:#FDBA74;--shiki-default-font-weight:bold;--shiki-light:#9A3412;--shiki-light-font-weight:bold}html pre.shiki code .swbo2, html code.shiki .swbo2{--shiki-default:#FBBF24;--shiki-light:#A16207}html pre.shiki code .sfVkF, html code.shiki .sfVkF{--shiki-default:#D6D3D1;--shiki-light:#57534E}html pre.shiki code .sotT8, html code.shiki .sotT8{--shiki-default:#FDBA74;--shiki-light:#9A3412}html pre.shiki code .sxJ4f, html code.shiki .sxJ4f{--shiki-default:#D6D3D1;--shiki-default-font-weight:bold;--shiki-light:#57534E;--shiki-light-font-weight:bold}html pre.shiki code .sile1, html code.shiki .sile1{--shiki-default:#FCD34D;--shiki-light:#7C2D12}html pre.shiki code .srOGa, html code.shiki .srOGa{--shiki-default:#A8A29E;--shiki-default-font-style:italic;--shiki-light:#6F6861;--shiki-light-font-style:italic}html pre.shiki code .sP85D, html code.shiki .sP85D{--shiki-default:#FEF3C7;--shiki-light:#78350F}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}",{"title":69,"searchDepth":101,"depth":101,"links":361},[362,363,364,365,366,367,368],{"id":21,"depth":101,"text":22},{"id":51,"depth":101,"text":52},{"id":294,"depth":101,"text":295},{"id":304,"depth":101,"text":305},{"id":320,"depth":101,"text":321},{"id":341,"depth":101,"text":342},{"id":348,"depth":101,"text":349},"2026-09-30","Over-the-air updates for a large ESP32 fleet: partition layout and rollback, signed images, staged rollouts and the cost of sending a megabyte to every device.","md","en",{},"/blog/updating-firmware-on-a-fleet-without-bricking-it","7 min read",{"title":6,"description":370},{"loc":374},"blog/updating-firmware-on-a-fleet-without-bricking-it",[380,381,382,383],"iot","esp32","firmware","ota","2026-10-01","5nFtdrra9VoQzHeAN4mxs0L7Uh3X7sEhatDSGKWx7J8",{"id":387,"title":388,"author":7,"body":389,"date":369,"description":657,"extension":371,"lang":658,"meta":659,"navigation":149,"path":374,"readingTime":660,"seo":661,"stem":378,"tags":662,"updated":384,"__hash__":663},"blogPl/blog/updating-firmware-on-a-fleet-without-bricking-it.md","Aktualizacja 50 000 ESP32 tak, żeby żadnego nie uceglić",{"type":9,"value":390,"toc":648},[391,394,397,401,413,419,422,426,432,435,581,584,588,591,594,598,601,604,607,610,614,617,626,629,633,636,640,643,646],[12,392,393],{},"Pierwsza aktualizacja OTA w nowym produkcie zwykle przechodzi gładko. Trafia na kilka płytek leżących na biurku, wszystkie mają dobre Wi-Fi i wszystkie stoją kilka metrów od osoby, która pisała kod. Aktualizacja, która naprawdę boli, przychodzi rok później. Flota liczy już dziesiątki tysięcy sztuk rozsianych po piwnicach, dachach i metalowych szafach, a drobna zmiana w stosie sieciowym sprawia, że kilka procent z nich nigdy więcej nie łączy się z siecią. Nikt nie pojedzie do tysiąca lokalizacji, żeby ręcznie przeflashować płytkę, więc cała ścieżka aktualizacji musi zakładać, że coś pójdzie nie tak, a mimo to zostawiać każde urządzenie w stanie, z którego da się wrócić do działania.",[12,395,396],{},"Poniżej opisuję, jak robię to na urządzeniach z ESP32. Większość da się przenieść na inne mikrokontrolery po niewielkich zmianach.",[19,398,400],{"id":399},"dwa-sloty-i-droga-odwrotu","Dwa sloty i droga odwrotu",[12,402,403,404,406,407,409,410,412],{},"Podstawą jest tablica partycji. ESP-IDF obsługuje dwa sloty aplikacji, zwykle nazwane ",[27,405,29],{}," i ",[27,408,33],{},", oraz małą partycję ",[27,411,37],{},", z której bootloader wie, który slot uruchomić. Urządzenie zawsze działa z jednego slotu, a nowy obraz pobiera do drugiego. Jeśli pobieranie przerwie się w połowie, nic się nie zmienia i urządzenie dalej działa na starym firmware. Dopiero gdy nowy obraz jest w całości zapisany i ma zweryfikowaną sumę kontrolną, urządzenie przestawia wskaźnik bootowania i się restartuje.",[12,414,415,416,418],{},"To rozwiązuje problem przerwanego pobierania. Nie pomaga jednak, gdy obraz pobierze się bez zarzutu, a potem po prostu nie działa, a to właśnie taki obraz ucegla urządzenia w terenie. Na to potrzebny jest mechanizm rollbacku w bootloaderze, włączany opcją ",[27,417,44],{},". Gdy jest aktywny, świeżo zainstalowany obraz startuje w stanie oczekiwania na weryfikację. Jeśli aplikacja potwierdzi, że działa poprawnie, obraz staje się obowiązującą wersją. Jeśli urządzenie zrestartuje się przed tym potwierdzeniem, bootloader uznaje nowy obraz za nieudany i sam wraca do poprzedniego slotu.",[12,420,421],{},"Całość sprowadza się więc do tego, co aplikacja uznaje za poprawne działanie.",[19,423,425],{"id":424},"kiedy-uznać-aktualizację-za-udaną","Kiedy uznać aktualizację za udaną",[12,427,428,429,431],{},"Widziałem firmware, który oznacza się jako poprawny już w pierwszej linii ",[27,430,58],{},". Chroni to przed obrazem, który wywala się, zanim tam dotrze, i właściwie przed niczym więcej. Urządzenie, które bez problemu startuje, ale potem już nigdy nie połączy się z brokerem, jest stracone tak samo jak to, które się wywala. Co więcej, to bardziej prawdopodobny scenariusz po aktualizacji, która dotykała Wi-Fi, certyfikatów TLS albo sterownika modemu.",[12,433,434],{},"Sam wolę potwierdzać obraz dopiero wtedy, gdy urządzenie zrobi to jedno, do czego zostało stworzone: połączy się z siecią, zakończy handshake TLS, dotrze do brokera MQTT i opublikuje wiadomość, której odbiór backend potwierdzi. Do tego czasu obraz pozostaje w stanie oczekiwania, a task watchdog albo zwykły timer restartuje urządzenie, jeśli trwa to zbyt długo, i w ten sposób odsyła je z powrotem na stary firmware.",[64,436,437],{"className":66,"code":67,"language":68,"meta":69,"style":69},[27,438,439,453,457,473,481,485,511,523,529,533,537,541,545,551,577],{"__ignoreMap":69},[73,440,441,443,445,447,449,451],{"class":75,"line":76},[73,442,80],{"class":79},[73,444,84],{"class":83},[73,446,88],{"class":87},[73,448,92],{"class":91},[73,450,95],{"class":83},[73,452,98],{"class":91},[73,454,455],{"class":75,"line":101},[73,456,104],{"class":91},[73,458,459,461,463,465,467,469,471],{"class":75,"line":107},[73,460,110],{"class":79},[73,462,114],{"class":113},[73,464,118],{"class":117},[73,466,122],{"class":121},[73,468,125],{"class":117},[73,470,128],{"class":87},[73,472,131],{"class":91},[73,474,475,477,479],{"class":75,"line":134},[73,476,137],{"class":113},[73,478,140],{"class":121},[73,480,143],{"class":91},[73,482,483],{"class":75,"line":146},[73,484,150],{"emptyLinePlaceholder":149},[73,486,487,489,491,493,495,497,499,501,503,505,507,509],{"class":75,"line":153},[73,488,156],{"class":79},[73,490,159],{"class":91},[73,492,162],{"class":87},[73,494,92],{"class":91},[73,496,167],{"class":87},[73,498,170],{"class":91},[73,500,173],{"class":117},[73,502,176],{"class":87},[73,504,179],{"class":91},[73,506,182],{"class":117},[73,508,185],{"class":121},[73,510,188],{"class":117},[73,512,513,515,517,519,521],{"class":75,"line":191},[73,514,194],{"class":121},[73,516,197],{"class":117},[73,518,200],{"class":121},[73,520,179],{"class":91},[73,522,205],{"class":91},[73,524,525,527],{"class":75,"line":208},[73,526,211],{"class":79},[73,528,143],{"class":91},[73,530,531],{"class":75,"line":216},[73,532,219],{"class":91},[73,534,535],{"class":75,"line":222},[73,536,150],{"emptyLinePlaceholder":149},[73,538,539],{"class":75,"line":227},[73,540,231],{"class":230},[73,542,543],{"class":75,"line":234},[73,544,237],{"class":230},[73,546,547,549],{"class":75,"line":240},[73,548,243],{"class":87},[73,550,131],{"class":91},[73,552,553,555,557,559,561,563,565,567,569,571,573,575],{"class":75,"line":248},[73,554,251],{"class":87},[73,556,92],{"class":91},[73,558,256],{"class":87},[73,560,170],{"class":91},[73,562,261],{"class":91},[73,564,265],{"class":264},[73,566,268],{"class":91},[73,568,170],{"class":91},[73,570,273],{"class":87},[73,572,276],{"class":91},[73,574,279],{"class":87},[73,576,282],{"class":91},[73,578,579],{"class":75,"line":285},[73,580,288],{"class":91},[12,582,583],{},"Timeout trzeba dobrać z głową. Urządzenie przy słabym zasięgu komórkowym potrafi rejestrować się w sieci nawet przez parę minut, więc limit trzydziestu sekund wycofywałby zupełnie dobre aktualizacje. Zwykle daję kilka minut, a urządzenie raportuje, z jaką wersją wróciło do sieci, dzięki czemu backend odróżni rollback od urządzenia, które po prostu zamilkło.",[19,585,587],{"id":586},"uruchamiaj-tylko-to-co-podpisałeś","Uruchamiaj tylko to, co podpisałeś",[12,589,590],{},"Wszystko, co potrafi zainstalować nowy kod przez sieć, jest jednocześnie powierzchnią ataku, a flocie, która przyjmuje niepodpisane obrazy, wystarczy jeden wyciek danych uwierzytelniających, żeby zaczęła uruchamiać cudzy firmware. W rodzinie ESP32 odpowiedzią jest Secure Boot v2. Przy każdym starcie sprawdza on podpis bootloadera i aplikacji, a także podpis każdego obrazu przychodzącego przez OTA, kluczem publicznym, którego skrót jest wypalony w eFuse'ach. Na oryginalnym ESP32 wymaga to układu w rewizji 3.0 lub nowszej, co warto sprawdzić przed pierwszym zamówieniem produkcyjnym. W parze z nim stosuje się szyfrowanie flasha, żeby firmware nie dało się po prostu odczytać z układu.",[12,592,593],{},"Obie te decyzje są nieodwracalne i podejmuje się je przy produkcji, dlatego lubię mieć je załatwione przed pierwszą partią. Dodanie secure boot do urządzeń, które już pracują w terenie, jest w najlepszym razie kłopotliwe, a w najgorszym niemożliwe. To samo dotyczy ochrony przed powrotem do starszej wersji (anti-rollback). ESP-IDF potrafi zapisać w eFuse'ach wersję bezpieczeństwa i odmówić uruchomienia czegokolwiek starszego, co uniemożliwia atakującemu wgranie starego obrazu ze znaną podatnością. Warto to włączyć, ale trzeba też używać tego ostrożnie, bo każde podbicie wersji na stałe wypala jeden bit. Na ESP32 to pole ma ich 32, więc wersję bezpieczeństwa podnoszę tylko w wydaniach, które łatają realną podatność.",[19,595,597],{"id":596},"wdrażanie-falami","Wdrażanie falami",[12,599,600],{},"Nawet z działającym rollbackiem nigdy nie wysyłam nowej wersji do całej floty naraz. Zły obraz, który czysto się wycofa, i tak kosztuje każde urządzenie jedno pobranie, restart i kilka minut offline. Do tego niektóre awarie ujawniają się dopiero po kilku godzinach, gdy wyciek pamięci zdąży urosnąć albo urządzenie przetrwa zimną noc.",[12,602,603],{},"Sprawdza mi się schemat, w którym najpierw aktualizuję kilkadziesiąt urządzeń wewnętrznych, potem około jednego procenta prawdziwej floty, następnie dziesięć procent, a na końcu wszystkie. Każda fala trwa na tyle długo, żeby objąć co najmniej jeden pełny cykl dobowy pracy urządzeń. Backend porównuje niewielki zestaw wskaźników zaktualizowanej grupy z resztą floty: przede wszystkim ile urządzeń wróciło z nową wersją, ile się wycofało, jak często łączą się ponownie, ile mają wolnej sterty i jak często zgłaszają crashe. Jeśli którykolwiek z nich zacznie odbiegać od normy, wdrożenie samo się zatrzymuje.",[12,605,606],{},"AWS IoT Jobs ma większość tego wbudowaną: tempo wdrażania może rosnąć z czasem, a reguły przerwania opierają się na odsetku urządzeń, u których aktualizacja się nie powiodła albo przekroczyła limit czasu. Jeśli masz własnego brokera, to samo zrobi niewielka usługa, która przechodzi listę urządzeń partiami i między partiami sprawdza kilka metryk. Nie wymaga to wiele kodu.",[12,608,609],{},"Fale lubię też dobierać celowo. Pierwsza prawdziwa grupa powinna obejmować najstarsze rewizje sprzętu, najsłabsze łącza i lokalizacje w najzimniejszych miejscach, bo właśnie tam problemy wychodzą najwcześniej. Grupa kanarkowa złożona wyłącznie ze zdrowych urządzeń w dobrych sieciach niewiele ci powie.",[19,611,613],{"id":612},"ile-kosztuje-megabajt","Ile kosztuje megabajt",[12,615,616],{},"Przy Wi-Fi nikt nie zastanawia się nad rozmiarem obrazu. Przy łączności komórkowej koszty szybko rosną. Gdy aplikacja na ESP32 zawiera stos TLS, klienta MQTT i logikę produktu, obraz o rozmiarze megabajta lub większym to norma. Przy półtora megabajta wysłanie go do pięćdziesięciu tysięcy urządzeń oznacza przesłanie mniej więcej siedemdziesięciu pięciu gigabajtów. Na taryfie IoT rozliczanej za megabajt pojedyncze wydanie może kosztować więcej niż miesiąc zwykłej telemetrii, a takich wydań flota i tak potrzebuje kilku w roku.",[12,618,619,620,622,623,625],{},"Pomaga tu kilka nawyków. Odchudzony obraz zwraca się przy każdej aktualizacji. Dzięki wznawianiu pobierania urządzenie, które straci połączenie przy dziewięćdziesięciu procentach, nie zaczyna od zera. Nowsze wersje ESP-IDF obsługują to w ",[27,621,330],{}," przez opcję ",[27,624,334],{},", która wznawia przerwane pobieranie żądaniem HTTP Range. Aktualizacje różnicowe (delta), w których urządzenie pobiera tylko różnicę między obecnym a nowym obrazem, potrafią zmniejszyć niewielką zmianę do ułamka pełnego rozmiaru. Komplikują jednak zarówno build, jak i firmware na urządzeniu, więc sięgnąłbym po nie dopiero wtedy, gdy rachunek za transfer wyraźnie to uzasadni.",[12,627,628],{},"Urządzenia bateryjne wymagają jeszcze jednej zasady: nigdy nie zaczynaj aktualizacji, gdy poziom naładowania spadł poniżej bezpiecznego progu. Zapis do flasha i kilkuminutowe utrzymywanie połączenia przez modem pobierają znacznie więcej prądu niż zwykła praca urządzenia, a bateria, która padnie w połowie, w najlepszym razie zostawi urządzenie na starym slocie.",[19,630,632],{"id":631},"co-musi-wiedzieć-backend","Co musi wiedzieć backend",[12,634,635],{},"Każde urządzenie powinno raportować wersję firmware, na której działa, slot, z którego wystartowało, i to, czy ostatni start nastąpił po rollbacku. Mając te dane, widzisz rozkład wersji we flocie na wykresie, a na pytania w rodzaju „ile urządzeń wciąż działa na wersji z marca” dostajesz odpowiedź w kilka sekund. Zapisuję też, które wydanie zaoferowano każdemu urządzeniu, kiedy je pobrało i kiedy potwierdziło. Gdy coś idzie nie tak, ta historia zwykle najszybciej ujawnia wzorzec, na przykład że wszystkie awarie dotyczą jednej rewizji sprzętu albo jednego operatora komórkowego.",[19,637,639],{"id":638},"od-czego-bym-zaczął","Od czego bym zaczął",[12,641,642],{},"Gdybym miał wskazać elementy najważniejsze w nowym produkcie, na pierwszym miejscu postawiłbym układ z dwoma slotami i rollbackiem, potwierdzanie aktualizacji dopiero po pełnej wymianie komunikatów z backendem oraz podpisane obrazy, bo dodanie ich później jest trudne albo wręcz niemożliwe. Wdrażanie etapami, wznawianie pobierania i raportowanie wersji we flocie można dobudować, gdy urządzenia już pracują w terenie.",[12,644,645],{},"Jeśli pracujesz nad strategią aktualizacji dla floty i chcesz ją z kimś przegadać, zawsze chętnie wymienię się doświadczeniami.",[357,647,359],{},{"title":69,"searchDepth":101,"depth":101,"links":649},[650,651,652,653,654,655,656],{"id":399,"depth":101,"text":400},{"id":424,"depth":101,"text":425},{"id":586,"depth":101,"text":587},{"id":596,"depth":101,"text":597},{"id":612,"depth":101,"text":613},{"id":631,"depth":101,"text":632},{"id":638,"depth":101,"text":639},"Aktualizacje OTA dużej floty ESP32: układ partycji i rollback, podpisane obrazy, wdrażanie etapami i ile kosztuje wysłanie megabajta na każde urządzenie.","pl",{},"7 min czytania",{"title":388,"description":657},[380,381,382,383],"e6gK2DhQ19oh9BuZ0c-xzFVanirrT7V9X_tD2i-K0no",[665,676,685,691,693,702,713,721],{"path":666,"stem":667,"title":668,"description":669,"date":670,"tags":671,"readingTime":675,"lang":658},"/blog/what-cra-compliance-takes-for-a-small-hardware-team","blog/what-cra-compliance-takes-for-a-small-hardware-team","Czego CRA naprawdę wymaga od małej firmy sprzętowej","Przeszedłem przez Cyber Resilience Act punkt po punkcie i zebrałem obowiązki producenta w jedną checklistę. Czego wymaga od urządzenia IoT i od czego zacząć.","2026-10-04",[672,380,673,674],"cra","security","compliance","8 min czytania",{"path":677,"stem":678,"title":679,"description":680,"date":384,"tags":681,"readingTime":684,"lang":658},"/blog/one-golden-certificate-for-the-whole-production-line","blog/one-golden-certificate-for-the-whole-production-line","Jeden złoty certyfikat na całą linię produkcyjną","Jak nadać każdemu urządzeniu własną tożsamość X.509 w AWS IoT Core bez wgrywania certyfikatów sztuka po sztuce: certyfikat claim i fleet provisioning.",[380,682,673,683,381],"aws","provisioning","9 min czytania",{"path":686,"stem":687,"title":688,"description":689,"date":384,"tags":690,"readingTime":684,"lang":658},"/blog/the-cra-clock-is-already-running","blog/the-cra-clock-is-already-running","Zegar CRA już tyka, także dla urządzeń sprzedanych lata temu","Od 11 września 2026 CRA wymaga zgłoszeń w ciągu 24 godzin dla każdego urządzenia IoT na rynku UE, także starego. Co to znaczy dla małej firmy sprzętowej?",[672,380,673,674],{"path":374,"stem":378,"title":388,"description":657,"date":369,"tags":692,"readingTime":660,"lang":658},[380,381,382,383],{"path":694,"stem":695,"title":696,"description":697,"date":369,"tags":698,"readingTime":660,"lang":658},"/blog/where-the-battery-goes-on-an-lte-m-sensor","blog/where-the-battery-goes-on-an-lte-m-sensor","Na co czujnik LTE-M zużywa baterię","Budżet energetyczny czujnika LTE-M na baterii: dlaczego dominuje w nim radio i które ustawienia decydują, czy bateria wytrzyma rok, czy pięć lat.",[380,699,700,701],"embedded","lte-m","low-power",{"path":703,"stem":704,"title":705,"description":706,"date":707,"tags":708,"readingTime":712,"lang":658},"/blog/building-an-llm-trading-bot","blog/building-an-llm-trading-bot","Wszyscy nagle budują boty tradingowe na LLM-ach","Ciągle słyszałem o modelach językowych, które same handlują, więc sprawdziłem, jak działają takie boty, gdzie się sypią i co zbudowałbym na ich miejscu.","2026-09-29",[709,710,711],"ai","llm","trading","6 min czytania",{"path":714,"stem":715,"title":716,"description":717,"date":707,"tags":718,"readingTime":720,"lang":658},"/blog/jev-a-model-that-only-decides","blog/jev-a-model-that-only-decides","JEV: model, który tylko decyduje","JEV od TypeSafe AI zwraca typowane odpowiedzi ze skalibrowanym prawdopodobieństwem i nie pisze ani słowa. Czym jest, gdzie się sprawdzi i na co uważać.",[709,710,719],"jev","5 min czytania",{"path":722,"stem":723,"title":724,"description":725,"date":707,"tags":726,"readingTime":675,"lang":658},"/blog/storing-a-reading-a-minute-from-100k-devices","blog/storing-a-reading-a-minute-from-100k-devices","Jak przechowywać minutowe odczyty ze 100 000 urządzeń","Co naprawdę trzeba zrobić, żeby przyjąć, zdeduplikować i przez rok trzymać minutową telemetrię ze 100 000 urządzeń, i gdzie kryją się koszty.",[380,727,728],"data-engineering","timescaledb",1792141330402]