SatNOGS-COMMS  4.1.0
A COMMS subsystem for CubeSats
Loading...
Searching...
No Matches
ota.cpp
Go to the documentation of this file.
1/*
2 * SatNOGS-COMMS MCU software
3 *
4 * Copyright (C) 2025, Libre Space Foundation <http://libre.space>
5 *
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 *
19 * SPDX-License-Identifier: GNU General Public License v3.0 or later
20 */
21
22#include "ota.hpp"
23#include "logger.hpp"
24#include "storage.hpp"
25#include <mbedtls/sha256.h>
26#include <scoped_lock.hpp>
27#include <startup.hpp>
28#include <zephyr/dfu/flash_img.h>
29#include <zephyr/dfu/mcuboot.h>
30#include <zephyr/retention/blinfo.h>
31#include <zephyr/retention/retention.h>
32#include <zephyr/storage/flash_map.h>
33#include <zephyr/storage/stream_flash.h>
34
35namespace satnogs::comms
36{
37
38static const struct device *otamem_dev =
39 DEVICE_DT_GET(DT_NODELABEL(ota_retention));
40
41static etl::string<CONFIG_STORAGE_MAX_PATH_LEN> littlefs_path;
42
43static mbedtls_sha256_context sha256_ctx;
44static uint8_t ota_buffer[CONFIG_OTA_BUFFER_SIZE];
45static etl::string<64> log_str;
46static struct flash_img_context flash_ctx_littlefs;
47static struct flash_img_context flash_ctx_slot0;
48static struct flash_img_context flash_ctx_slot1;
49
50static uint8_t slots_to_area[2] = {FIXED_PARTITION_ID(slot0_partition),
51 FIXED_PARTITION_ID(slot1_partition)};
52
53void
55{
56 scoped_lock lock(&m_mtx);
57 if (id > CONFIG_OTA_MAX_SESSIONS - 1) {
58 throw inval_session(__FILE__, __LINE__);
59 }
60 session s{};
61 m_sessions[id] = s;
62 update_retention(id);
63}
64
65void
67{
68 for (uint32_t i = 0; i < CONFIG_OTA_MAX_SESSIONS; i++) {
70 }
71}
72
73bool
74ota::get_session_info(uint8_t id, session &out) const
75{
76 scoped_lock lock(&m_mtx);
77 if (id > CONFIG_OTA_MAX_SESSIONS - 1)
78 return false;
79 out = m_sessions[id];
80 return true;
81}
82
83void
85{
86 scoped_lock lock(&m_mtx);
87 response_tlm resp;
88 uint32_t idx = 0;
89 bool found = false;
90 /* Try to find a free session */
91 for (uint32_t i = 0; i < CONFIG_OTA_MAX_SESSIONS; i++) {
92 if (m_sessions[i].active == false || m_sessions[i].applied) {
93 idx = i;
94 found = true;
95 break;
96 }
97 }
98 if (!found) {
100 tlm = resp;
101 return;
102 }
103
104 /*
105 * Make some sanity checks. SatNOGS-COMMS uses the XIP method for the MCU.
106 * Therefore, before any attempt to perform an OTA for the MCU, we should be
107 * sure that we do not update the slot that the board currently operates
108 *
109 * NOTE: It seems that there is a naming conflict between the slot number
110 * considered by the MCUBoot and the partition ID. MCUBoot uses the the slot 0
111 * for the slot0_partition and slot 1 for the slot1_partition. However, the
112 * partition IDs are not following the same convention. Therefore we need to
113 * use the `slots_to_area` array to map the slot number to the partition ID.
114 * `slots_to_area[0]` is the partition ID for slot 0 and `slots_to_area[1]` is
115 * the partition ID for slot 1.
116 *
117 * NOTE 2: flash_img_get_upload_slot() has a lame implementation and for sure
118 * is not suitable for XIP setups. DO NOT USE IT!
119 */
120 auto &log = logger::get_instance();
121
122 switch (req.dst) {
126 if (!boot_is_img_confirmed()) {
128 tlm = resp;
129 return;
130 }
131 } break;
132 default:
133 break;
134 }
135
136 switch (req.dst) {
138 case subsys::MCU_SLOT1: {
139 uint8_t active_slot = boot_fetch_active_slot();
140 if (active_slot == slots_to_area[req.dst == subsys::MCU_SLOT0 ? 0 : 1]) {
141 log_str.clear();
142 etl::string_stream stream(log_str);
143 stream << "Active slot (" << active_slot << ") cannot be updated";
144 log.log(log_str);
145 resp.s = status::INVALID_SLOT;
146 tlm = resp;
147 return;
148 }
149 int ret = flash_img_init_id(
150 req.dst == subsys::MCU_SLOT0 ? &flash_ctx_slot0 : &flash_ctx_slot1,
151 slots_to_area[req.dst == subsys::MCU_SLOT0 ? 0 : 1]);
152 if (ret) {
153 log_str.clear();
154 etl::string_stream stream(log_str);
155 stream << "flash_img_init_id() error " << ret;
156 log.log(log_str);
158 tlm = resp;
159 return;
160 }
161 } break;
162 case subsys::FPGA_WIC:
163 break;
167 /*
168 * Make sure that the directory for the firmwares exists. If it is already
169 * created, mkdir() does not produce any error
170 */
171 littlefs_path.clear();
172 littlefs_path.append(req.dst == subsys::MCU_LITTLEFS ? LITTLEFS_MCU_DIR
174 storage.mkdir(littlefs_path);
175
176 /*
177 * If the destination is a file at the littleFS storage, delete any file
178 * that may have the same name, leftover from a previous OTA session
179 */
180
181 littlefs_path.clear();
182 etl::string_stream stream(littlefs_path);
183 stream << (req.dst == subsys::MCU_LITTLEFS ? LITTLEFS_MCU_DIR
185 << req.name;
186 try {
187
188 storage.rm(littlefs_path);
189 } catch (const storage::delete_file_exception &e) {
190 }
191
192 } break;
193 default:
194 log.log("Invalid OTA subsystem");
195 resp.s = status::INVALID_DST;
196 tlm = resp;
197 return;
198 }
199
200 m_sessions[idx] = session{};
201 m_sessions[idx].active = true;
202 std::copy_n(req.sha256, 32, m_sessions[idx].sha256);
203 m_sessions[idx].total_size = req.size;
204 m_sessions[idx].name = req.name;
205 m_sessions[idx].destination = req.dst;
206 m_sessions[idx].slot = req.slot;
207 tlm.s = status::OK;
208 tlm.ack = 0;
209 tlm.session = idx;
210 update_retention(idx);
211}
212
213void
215{
216 scoped_lock lock(&m_mtx);
217 tlm.session = tlc.session;
218
219 /* Perform all sanity checks first */
220 if (tlc.session > CONFIG_OTA_MAX_SESSIONS - 1) {
222 return;
223 }
224
225 auto &ctx = m_sessions[tlc.session];
226 if (ctx.active == false) {
228 return;
229 }
230
231 /* Perform simple sequence number management */
232 if (tlc.seq != ctx.ack) {
234 tlm.ack = ctx.ack;
235 return;
236 }
237
238 switch (ctx.destination) {
242 if (!boot_is_img_confirmed()) {
244 tlm.ack = ctx.ack;
245 return;
246 }
247 } break;
248 default:
249 break;
250 }
251
252 /* Copy data and progress */
253 auto res = status::OK;
254 switch (ctx.destination) {
257 res = write_slot(ctx, tlc);
258 break;
259 case subsys::FPGA_WIC:
260 res = write_wic(ctx, tlc);
261 break;
263 res = write_littlefs(true, ctx, tlc);
264 break;
266 res = write_littlefs(false, ctx, tlc);
267 break;
268 default:
270 tlm.ack = ctx.ack;
271 return;
272 }
273
274 tlm.s = res;
275 tlm.ack = ctx.ack;
276 update_retention(tlc.session);
277}
278
279void
280ota::finalize(const fin_tlc &tlc, response_tlm &tlm, int wdgid)
281{
282 scoped_lock lock(&m_mtx);
283 tlm.session = tlc.session;
284 tlm.ack = 0;
285
286 /* Perform all sanity checks first */
287 if (tlc.session > CONFIG_OTA_MAX_SESSIONS - 1) {
289 return;
290 }
291
292 auto &ctx = m_sessions[tlc.session];
293 if (ctx.active == false) {
295 return;
296 }
297
298 tlm.ack = ctx.ack;
299 if (ctx.ack != ctx.total_size) {
301 return;
302 }
303
304 if (sha256_check(ctx, wdgid) == false) {
306 return;
307 }
308
309 tlm.s = status::OK;
310
311 /* Apply the update asynchronously */
312 switch (ctx.destination) {
314 case subsys::MCU_SLOT1: {
315 if (!boot_is_img_confirmed()) {
317 tlm.ack = ctx.ack;
318 return;
319 }
320 finalize_mcu(ctx, tlm);
321 } break;
323 if (!boot_is_img_confirmed()) {
325 tlm.ack = ctx.ack;
326 return;
327 }
328 finalize_littlefs_to_mcu(ctx, tlm);
329 } break;
330 default:
331 break;
332 }
333}
334
345void
347{
348 boot_write_img_confirmed();
349}
350
352{
353 static_assert(sizeof(session) * CONFIG_OTA_MAX_SESSIONS <
355 k_mutex_init(&m_mtx);
356
357 if (retention_is_valid(otamem_dev)) {
358 /* Get the number of stored sessions and the hash ID od the session class.
359 * If any of them is different, previous firmware used a different
360 * configuration. If this is the case, we cannot do much. Just reset
361 * everything.
362 */
363 uint32_t n = 0;
364 retention_read(otamem_dev, 0, reinterpret_cast<uint8_t *>(&n),
365 sizeof(uint32_t));
366 uint32_t hash = 0;
367 retention_read(otamem_dev, sizeof(uint32_t),
368 reinterpret_cast<uint8_t *>(&hash), sizeof(uint32_t));
369 if (n != CONFIG_OTA_MAX_SESSIONS || hash != session::HASH) {
370 reset_retention();
371 } else {
372 /* Restore everything from the retention memory */
373 for (uint32_t i = 0; i < CONFIG_OTA_MAX_SESSIONS; i++) {
374 session s{};
375 retention_read(otamem_dev, 2 * sizeof(uint32_t) + i * sizeof(s),
376 reinterpret_cast<uint8_t *>(&s), sizeof(s));
377 /*
378 * Retrieving an etl::string with low level access needs special care
379 * https://www.etlcpp.com/string.html
380 */
381 s.name.repair();
382 m_sessions[i] = s;
383 /* Slot 0 and 1 do not support recovering from reboot. This has to do
384 * mainly with the flash_img_init_id() which has to be called at the
385 * start of an OTA session.
386 * But if a power cycle has occurred, it is not possible to continue a
387 * previously running session */
388 if (m_sessions[i].destination == subsys::MCU_SLOT0 ||
389 m_sessions[i].destination == subsys::MCU_SLOT1) {
390 reset_session(i);
391 }
392 }
393 }
394 } else {
395 reset_retention();
396 }
397}
398
403void
404ota::reset_retention()
405{
406 retention_clear(otamem_dev);
407 /* Apply number of possible sessions */
408 uint32_t x = CONFIG_OTA_MAX_SESSIONS;
409 retention_write(otamem_dev, 0, reinterpret_cast<const uint8_t *>(&x),
410 sizeof(uint32_t));
411 /* Apply session class hash */
412 x = session::HASH;
413 retention_write(otamem_dev, sizeof(uint32_t),
414 reinterpret_cast<const uint8_t *>(&x), sizeof(uint32_t));
415
416 /* Apply default inactive sessions*/
417 for (uint32_t i = 0; i < CONFIG_OTA_MAX_SESSIONS; i++) {
418 session s{};
419 retention_write(otamem_dev, 2 * sizeof(uint32_t) + i * sizeof(s),
420 reinterpret_cast<const uint8_t *>(&s), sizeof(s));
421 }
422}
423
424void
425ota::update_retention(uint32_t idx)
426{
427 retention_write(otamem_dev, 2 * sizeof(uint32_t) + idx * sizeof(session),
428 reinterpret_cast<const uint8_t *>(&m_sessions[idx]),
429 sizeof(session));
430}
431
433ota::write_slot(session &s, const data_tlc &tlc)
434{
435 auto &log = logger::get_instance();
436 /* A bit pedantic but better safe than sorry */
437 uint8_t active_slot = boot_fetch_active_slot();
438 if (active_slot ==
439 slots_to_area[s.destination == subsys::MCU_SLOT0 ? 0 : 1]) {
440 log_str.clear();
441 etl::string_stream stream(log_str);
442 stream << "Cannot write to active slot " << active_slot;
443 log.log(log_str);
445 }
446
447 switch (s.destination) {
449 flash_img_buffered_write(&flash_ctx_slot0, tlc.data, tlc.len, false);
450 break;
452 flash_img_buffered_write(&flash_ctx_slot1, tlc.data, tlc.len, false);
453 break;
454 /* In case of invalid destination do not advance the ack*/
455 default:
456 log.log("Invalid destination. Only MCU_SLOT0 and MCU_SLOT1 are "
457 "allowed");
459 }
460 s.ack += tlc.len;
461 return status::OK;
462}
463
465ota::write_wic(session &s, const data_tlc &tlc)
466{
467 uint32_t ncopy = tlc.len;
468 if (s.ack + tlc.len > s.total_size) {
469 ncopy = s.ack + tlc.len - s.total_size;
470 }
471
472 auto &storage = storage::get_instance();
473 uint32_t written =
474 storage.write_raw(tlc.data, s.ack / storage::SECTOR_SIZE, ncopy);
475
476 s.ack += written;
477 return status::OK;
478}
479
481ota::write_littlefs(bool mcu, session &s, const data_tlc &tlc)
482{
483 uint32_t ncopy = tlc.len;
484 if (s.ack + tlc.len > s.total_size) {
485 ncopy = s.ack + tlc.len - s.total_size;
486 }
487
488 littlefs_path.clear();
489 etl::string_stream stream(littlefs_path);
490 stream << (mcu ? LITTLEFS_MCU_DIR : LITTLEFS_FPGA_DIR) << s.name;
491
492 auto &storage = storage::get_instance();
493 int ret = storage.write(littlefs_path, tlc.data, ncopy);
494 if (ret < 0) {
496 }
497
498 uint32_t written = static_cast<uint32_t>(ret);
499 s.ack += written;
500 return status::OK;
501}
502
511bool
512ota::sha256_check(const session &s, int wdgid)
513{
514 switch (s.destination) {
517 return sha256_mcu(s);
518 case subsys::FPGA_WIC:
519 return sha256_fpga_wic(s, wdgid);
522 littlefs_path.clear();
523 etl::string_stream stream(littlefs_path);
524 stream << (s.destination == subsys::MCU_LITTLEFS ? LITTLEFS_MCU_DIR
526 << s.name;
527 return sha256_littlefs(s, littlefs_path, wdgid);
528 }
529 default:
530 return false;
531 }
532}
533
534bool
535ota::sha256_littlefs(const session &s, const etl::istring &path, int wdgid)
536{
537 auto ts = k_uptime_seconds();
538 mbedtls_sha256_init(&sha256_ctx);
539 mbedtls_sha256_starts(&sha256_ctx, 0);
540 auto &storage = storage::get_instance();
541 const uint32_t iters = s.total_size / CONFIG_OTA_BUFFER_SIZE;
542 storage::file f(path);
543 for (uint32_t i = 0; i < iters; i++) {
544 /*
545 * Reset every 1 second the task watchdog and let other threads to do
546 * their job
547 */
548 auto now = k_uptime_seconds();
549 if (now - ts > 1) {
550 task_wdt_feed(wdgid);
551 k_msleep(1);
552 ts = now;
553 }
554 storage.read(f, ota_buffer, CONFIG_OTA_BUFFER_SIZE);
555 mbedtls_sha256_update(&sha256_ctx, ota_buffer, CONFIG_OTA_BUFFER_SIZE);
556 }
557
558 if (s.total_size % CONFIG_OTA_BUFFER_SIZE) {
559 storage.read(f, ota_buffer, s.total_size % CONFIG_OTA_BUFFER_SIZE);
560 mbedtls_sha256_update(&sha256_ctx, ota_buffer,
561 s.total_size % CONFIG_OTA_BUFFER_SIZE);
562 }
563 mbedtls_sha256_finish(&sha256_ctx, ota_buffer);
564 mbedtls_sha256_free(&sha256_ctx);
565 return std::equal(s.sha256, s.sha256 + 32, ota_buffer);
566}
567
568bool
569ota::sha256_fpga_wic(const session &s, int wdgid)
570{
571 auto ts = k_uptime_seconds();
572 mbedtls_sha256_init(&sha256_ctx);
573 mbedtls_sha256_starts(&sha256_ctx, 0);
574 auto &storage = storage::get_instance();
575 const uint32_t nsectors = s.total_size / storage::SECTOR_SIZE;
576 for (uint32_t i = 0; i < nsectors; i++) {
577 /*
578 * Reset every 1 second the task watchdog and let other threads to do
579 * their job
580 */
581 auto now = k_uptime_seconds();
582 if (now - ts > 1) {
583 task_wdt_feed(wdgid);
584 k_msleep(1);
585 ts = now;
586 }
587 storage.read_raw(ota_buffer, i, 1);
588 mbedtls_sha256_update(&sha256_ctx, ota_buffer, storage::SECTOR_SIZE);
589 }
590
591 if (s.total_size % storage::SECTOR_SIZE) {
592 storage.read_raw(ota_buffer, nsectors, 1);
593 mbedtls_sha256_update(&sha256_ctx, ota_buffer,
594 s.total_size % storage::SECTOR_SIZE);
595 }
596 mbedtls_sha256_finish(&sha256_ctx, ota_buffer);
597 mbedtls_sha256_free(&sha256_ctx);
598 return std::equal(s.sha256, s.sha256 + 32, ota_buffer);
599}
600
601bool
602ota::sha256_mcu(const session &s)
603{
604 struct flash_img_check check;
605 check.clen = s.total_size;
606 check.match = s.sha256;
607 /*
608 * Flush any outstanding bytes. Note that we use the ota_buffer as a dummy
609 * buffer. After that check the integrity of the image and proceed with the
610 * update
611 */
612 int ret = 0;
613 if (s.slot == 0) {
614 flash_img_buffered_write(&flash_ctx_slot0, ota_buffer, 0, true);
615 ret = flash_img_check(&flash_ctx_slot0, &check, slots_to_area[0]);
616 } else {
617 flash_img_buffered_write(&flash_ctx_slot1, ota_buffer, 0, true);
618 ret = flash_img_check(&flash_ctx_slot1, &check, slots_to_area[1]);
619 }
620 return ret == 0;
621}
622
623bool
624ota::finalize_littlefs_to_mcu(session &s, response_tlm &tlm)
625{
626 auto &log = logger::get_instance();
627 if (s.slot > 1) {
628 log_str.clear();
629 etl::string_stream stream(log_str);
630 stream << "Invalid slot number " << s.slot;
631 log.log(log_str);
632 tlm.s = status::INVALID_SLOT;
633 return false;
634 }
635
636 /*
637 * Check the current slot and the target one. Obviously, the should not be
638 * the same
639 */
640 uint8_t curr_slot = boot_fetch_active_slot();
641 if (curr_slot == slots_to_area[s.slot]) {
642 log_str.clear();
643 etl::string_stream stream(log_str);
644 stream << "Active slot and update slot is the same (" << s.slot << ")";
645 log.log(log_str);
646 tlm.s = status::INVALID_SLOT;
647 return false;
648 }
649
650 int ret = flash_img_init_id(&flash_ctx_littlefs, slots_to_area[s.slot]);
651 if (ret) {
652 log_str.clear();
653 etl::string_stream stream(log_str);
654 stream << "flash_img_init_id() error " << ret;
655 log.log(log_str);
657 return false;
658 }
659
660 littlefs_path.clear();
661 etl::string_stream stream(littlefs_path);
662 stream << LITTLEFS_MCU_DIR << s.name;
663 storage::file f(littlefs_path);
664 const uint32_t iters = s.total_size / CONFIG_IMG_BLOCK_BUF_SIZE;
665 auto &storage = storage::get_instance();
666 for (uint32_t i = 0; i < iters; i++) {
667 storage.read(f, ota_buffer, CONFIG_IMG_BLOCK_BUF_SIZE);
668 flash_img_buffered_write(&flash_ctx_littlefs, ota_buffer,
669 CONFIG_IMG_BLOCK_BUF_SIZE, false);
670 }
671 storage.read(f, ota_buffer, s.total_size % CONFIG_IMG_BLOCK_BUF_SIZE);
672 flash_img_buffered_write(&flash_ctx_littlefs, ota_buffer,
673 s.total_size % CONFIG_IMG_BLOCK_BUF_SIZE, true);
674
675 boot_request_upgrade(BOOT_UPGRADE_TEST);
676 k_msleep(2000);
677 sys_reboot(SYS_REBOOT_COLD);
678 tlm.s = status::OK;
679 return true;
680}
681
682bool
683ota::finalize_mcu(session &s, response_tlm &tlm)
684{
685 /* sha256_mcu() has already done the buffer flush */
686 int ret = boot_request_upgrade(BOOT_UPGRADE_TEST);
687 if (ret == 0) {
688 s.applied = true;
689 }
690 sys_reboot(SYS_REBOOT_COLD);
691 return ret == 0;
692}
693
694} // namespace satnogs::comms
static logger & get_instance()
Singleton access to the logger subsystem.
Definition logger.hpp:102
etl::string< MAX_FILE_LEN > name
Definition ota.hpp:174
static constexpr size_t HASH
Definition ota.hpp:151
etl::string< MAX_FILE_LEN > name
Definition ota.hpp:103
void recv(const data_tlc &tlc, response_tlm &tlm)
Definition ota.cpp:214
static constexpr const char * LITTLEFS_FPGA_DIR
Definition ota.hpp:48
void finalize(const fin_tlc &tlc, response_tlm &tlm, int wdgid)
Definition ota.cpp:280
void reset_session(uint32_t id)
Definition ota.cpp:54
static constexpr const char * LITTLEFS_MCU_DIR
Definition ota.hpp:47
static constexpr size_t OTA_RETENTION_MEM_SIZE
The available retention memory for the OTA service.
Definition ota.hpp:216
@ MCU_SLOT1
MCU MCUBoot Slot 1.
Definition ota.hpp:69
@ MCU_SLOT0
MCU MCUBoot Slot 0.
Definition ota.hpp:68
void start(const start_tlc &req, response_tlm &tlm)
Definition ota.cpp:84
void confirm_image()
Sets the running firmware as confirmed.
Definition ota.cpp:346
bool get_session_info(uint8_t id, session &out) const
Definition ota.cpp:74
void reset_sessions()
Definition ota.cpp:66
Implements a scoped lock utilizing the Zephyr mutex.
void mkdir(const etl::istring &path)
Creates a directory at the specified path.
Definition storage.cpp:248
static storage & get_instance()
Definition storage.hpp:94
void rm(const etl::istring &path)
Removes a file from the filesystem.
Definition storage.cpp:295
static constexpr size_t SECTOR_SIZE
Definition storage.hpp:36