SatNOGS-COMMS  4.1.0
A COMMS subsystem for CubeSats
Loading...
Searching...
No Matches
storage.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 "storage.hpp"
23#include "scoped_lock.hpp"
24#include "settings.hpp"
25#include "time.hpp"
26
27#include <time.h>
28#include <zephyr/drivers/disk.h>
29#include <zephyr/fs/fs.h>
30#include <zephyr/fs/littlefs.h>
31#include <zephyr/kernel.h>
32#include <zephyr/storage/disk_access.h>
33
34#include <etl/basic_string.h>
35#include <etl/format_spec.h>
36#include <etl/queue.h>
37#include <etl/stack.h>
38#include <etl/string.h>
39#include <etl/string_stream.h>
40#include <etl/to_string.h>
41
42namespace satnogs::comms
43{
44
49
50static struct fs_mount_t storage_mnt = {
51 .type = FS_LITTLEFS,
52 .mnt_point = CONFIG_STORAGE_MNT_POINT,
53 .fs_data = &lfs_storage,
54 .storage_dev = (void *)CONFIG_DISK_NAME,
55 .flags = FS_MOUNT_FLAG_USE_DISK_ACCESS | FS_MOUNT_FLAG_NO_FORMAT,
56};
57
58/* Raw access uses DMA and needs an aligned buffer */
59alignas(16) static uint8_t aligned_buffer[CONFIG_MAX_MTU];
60
61/* Used for DFS traversal */
62static etl::queue<etl::string<CONFIG_STORAGE_MAX_PATH_LEN>,
63 CONFIG_STORAGE_MAX_DEPTH>
64 m_dfs_dirs;
65
66storage::storage() : m_enabled(false), m_mounted(false)
67{
68 m_pname.clear();
69 k_mutex_init(&m_mtx);
70}
71
79float
81{
82 if (!ready()) {
83 throw not_ready_exception(__FILE__, __LINE__);
84 }
85 const etl::string<16> emmc_dirpath(CONFIG_STORAGE_MNT_POINT);
86 const uint64_t used_bytes = du(emmc_dirpath);
87 const uint64_t total_bytes =
88 (sectors_num() - CONFIG_DISK_START_SECTOR) * 512ULL;
89
90 return static_cast<float>(used_bytes) / static_cast<float>(total_bytes);
91}
92
93void
94storage::set_dir_priv(lib::emmc::dir d, bool mnt)
95{
96 auto &emmc = lib::board::get_instance().emmc();
97 /* When the direction switches to FPGA the filesystem should be unmounted */
98 if (d == lib::emmc::dir::FPGA) {
99 if (m_enabled && m_mounted) {
100 fs_unmount(&storage_mnt);
101 m_mounted = false;
102 }
103 }
104 emmc.set_dir(d);
105 /* If requested, try to mount also in case we switch back to the MCU */
106 if (m_enabled && mnt && d == lib::emmc::dir::MCU) {
107 int ret = fs_mount(&storage_mnt);
108 if (ret == 0 || ret == -EBUSY) {
109 m_mounted = true;
110 }
111 }
112}
113
124void
126{
127 /* If already enabled, don't mess anything */
128 if (enabled() && en) {
129 return;
130 }
131
132 auto &emmc = lib::board::get_instance().emmc();
133 scoped_lock lock(&m_mtx);
134 if (en) {
135 emmc.enable(true);
136 emmc.reset(true);
137 k_msleep(10);
138 emmc.reset(false);
139 set_dir_priv(lib::emmc::dir::MCU);
140 m_enabled = true;
141
142 int rc = fs_mount(&storage_mnt);
143
144 if (rc == -EBUSY) {
145 /* Accept the already mounted case! */
146 throw mount_exception(__FILE__, __LINE__, rc);
147 } else if (rc < 0) {
148 /* Try and format the disk */
149 storage_mnt.flags = storage_mnt.flags & ~FS_MOUNT_FLAG_NO_FORMAT;
150 rc = fs_mount(&storage_mnt);
151 if (rc < 0) {
152 throw mount_exception(__FILE__, __LINE__, rc);
153 }
154 /* reset the flag */
155 storage_mnt.flags |= FS_MOUNT_FLAG_NO_FORMAT;
156 }
157
158 m_pname.clear();
159 m_pname.append(storage_mnt.mnt_point);
160 m_pname.append(CONFIG_STORAGE_LOG_MCU_PATH);
161 m_mounted = true;
162 mkdir(m_pname);
163
164 } else {
165 int rc = fs_unmount(&storage_mnt);
166 disk_access_ioctl(CONFIG_DISK_NAME, DISK_IOCTL_CTRL_DEINIT, NULL);
167 m_mounted = false;
168 emmc.enable(false);
169 m_enabled = false;
170 if (rc < 0 && rc != -EINVAL) {
171 throw mount_exception(__FILE__, __LINE__, rc);
172 }
173 }
174}
175
182bool
184{
185 return m_enabled;
186}
187
194bool
196{
197 return m_mounted;
198}
199
205void
207{
208 scoped_lock lock(&m_mtx);
209 set_dir_priv(d, true);
210}
211
219{
220 auto &emmc = lib::board::get_instance().emmc();
221 scoped_lock lock(&m_mtx);
222 return emmc.get_dir();
223}
224
233bool
235{
236 return enabled() && get_dir() == lib::emmc::dir::MCU && mounted();
237}
238
247void
248storage::mkdir(const etl::istring &path)
249{
250
251 if (!ready()) {
252 throw not_ready_exception(__FILE__, __LINE__);
253 }
254
255 if (path.size() >= CONFIG_STORAGE_MAX_PATH_LEN) {
256 throw max_path_exception(__FILE__, __LINE__);
257 }
258
259 scoped_lock lock(&m_mtx);
260
261 int rc = fs_mkdir(path.c_str());
262 if (rc < 0 && rc != -EEXIST) {
263 throw fs_exception(__FILE__, __LINE__);
264 }
265}
266
267int
268storage::rm_priv(const etl::istring &path)
269{
270 return fs_unlink(path.c_str());
271}
272
280bool
281storage::ready_raw() const
282{
283 return enabled() && get_dir() == lib::emmc::dir::MCU;
284}
285
294void
295storage::rm(const etl::istring &path)
296{
297 if (!ready()) {
298 throw not_ready_exception(__FILE__, __LINE__);
299 }
300
301 if (path.size() > CONFIG_STORAGE_MAX_PATH_LEN) {
302 throw max_path_exception(__FILE__, __LINE__);
303 }
304
305 struct fs_dirent entry;
306 int rc = fs_stat(path.c_str(), &entry);
307 if (rc < 0 || entry.type != FS_DIR_ENTRY_FILE) {
308 throw delete_file_exception(__FILE__, __LINE__);
309 }
310
311 scoped_lock lock(&m_mtx);
312 rc = rm_priv(path);
313
314 if (rc < 0) {
315 throw delete_file_exception(__FILE__, __LINE__);
316 }
317}
318
328void
329storage::rmdir(const etl::istring &path)
330{
331 if (!ready()) {
332 throw not_ready_exception(__FILE__, __LINE__);
333 }
334
335 if (path.size() >= CONFIG_STORAGE_MAX_PATH_LEN) {
336 throw max_path_exception(__FILE__, __LINE__);
337 }
338
339 static struct fs_dirent entry;
340
341 int rc = fs_stat(path.c_str(), &entry);
342 if (rc < 0 || entry.type != FS_DIR_ENTRY_DIR) {
343 throw delete_dir_exception(__FILE__, __LINE__);
344 }
345
346 etl::string_stream stream(m_pname);
347
348 struct fs_dir_t dirp;
349 fs_dir_t_init(&dirp);
350
351 scoped_lock lock(&m_mtx);
352 rc = fs_opendir(&dirp, path.c_str());
353 if (rc) {
354 throw fs_exception(__FILE__, __LINE__);
355 }
356
357 for (;;) {
358 rc = fs_readdir(&dirp, &entry);
359
360 if (rc || entry.name[0] == 0) {
361 fs_closedir(&dirp);
362 if (rc < 0) {
363 throw fs_exception(__FILE__, __LINE__);
364 }
365 break;
366 }
367
368 m_pname.clear();
369 stream << path << "/" << entry.name;
370 /*
371 * Try to delete it if it is a file
372 * FIXME: Currently we do not support recursive file deletion
373 */
374 if (entry.type == FS_DIR_ENTRY_FILE) {
375 rm_priv(m_pname);
376 }
377 }
378
379 fs_closedir(&dirp);
380 rc = fs_unlink(path.c_str());
381 if (rc < 0) {
382 throw delete_dir_exception(__FILE__, __LINE__);
383 }
384}
385
394size_t
395storage::write(const etl::istring &path, const uint8_t *b, size_t len)
396{
397 if (!ready()) {
398 throw not_ready_exception(__FILE__, __LINE__);
399 }
400
401 int rc;
402 if (path.size() >= CONFIG_STORAGE_MAX_PATH_LEN) {
403 throw max_path_exception(__FILE__, __LINE__);
404 }
405 struct fs_file_t file;
406
407 scoped_lock lock(&m_mtx);
408
409 fs_file_t_init(&file);
410 rc = fs_open(&file, path.c_str(), FS_O_CREATE | FS_O_RDWR);
411 if (rc < 0) {
412 fs_close(&file);
413 throw fs_exception(__FILE__, __LINE__);
414 }
415
416 fs_seek(&file, 0, FS_SEEK_END);
417 len = std::min<size_t>(len, CONFIG_MAX_MTU);
418 std::copy_n(b, len, aligned_buffer);
419 rc = fs_write(&file, aligned_buffer, len);
420 if (rc < 0) {
421 fs_close(&file);
422 throw fs_exception(__FILE__, __LINE__);
423 }
424
425 fs_close(&file);
426 return rc;
427}
428
444void
445storage::truncate(const etl::istring &path, size_t len)
446{
447 if (!ready()) {
448 throw not_ready_exception(__FILE__, __LINE__);
449 }
450
451 int rc;
452 if (path.size() >= CONFIG_STORAGE_MAX_PATH_LEN) {
453 throw max_path_exception(__FILE__, __LINE__);
454 }
455
456 // Compute new size (clamp at 0)
457 uint64_t curr_size = du(path);
458 const off_t new_size =
459 (curr_size > len) ? static_cast<off_t>(curr_size - len) : 0;
460
461 // Init and open the file
462 struct fs_file_t file;
463 scoped_lock lock(&m_mtx);
464
465 fs_file_t_init(&file);
466 rc = fs_open(&file, path.c_str(), FS_O_RDWR);
467 if (rc < 0) {
468 fs_close(&file);
469 throw fs_exception(__FILE__, __LINE__);
470 }
471
472 // Truncate the file
473 rc = fs_truncate(&file, new_size);
474 if (rc < 0) {
475 fs_close(&file);
476 throw fs_exception(__FILE__, __LINE__);
477 }
478
479 // Ensure metadata is flushed (not necessary, but just to be sure)
480 rc = fs_sync(&file);
481 fs_close(&file);
482 if (rc < 0) {
483 throw fs_exception(__FILE__, __LINE__);
484 }
485}
486
500size_t
501storage::write(const etl::istring &path, const uint8_t *b, size_t len,
502 size_t offset)
503{
504 if (!ready()) {
505 throw not_ready_exception(__FILE__, __LINE__);
506 }
507
508 int rc;
509 if (path.size() >= CONFIG_STORAGE_MAX_PATH_LEN) {
510 throw max_path_exception(__FILE__, __LINE__);
511 }
512 struct fs_file_t file;
513
514 scoped_lock lock(&m_mtx);
515
516 fs_file_t_init(&file);
517 rc = fs_open(&file, path.c_str(), FS_O_CREATE | FS_O_RDWR);
518 if (rc < 0) {
519 fs_close(&file);
520 throw fs_exception(__FILE__, __LINE__);
521 }
522
523 /* Try to seek, otherwise append at the end */
524 rc = fs_seek(&file, offset, FS_SEEK_SET);
525 if (rc < 0) {
526 fs_seek(&file, 0, FS_SEEK_END);
527 }
528
529 len = std::min<size_t>(len, CONFIG_MAX_MTU);
530 std::copy_n(b, len, aligned_buffer);
531 rc = fs_write(&file, aligned_buffer, len);
532 if (rc < 0) {
533 fs_close(&file);
534 throw fs_exception(__FILE__, __LINE__);
535 }
536
537 fs_close(&file);
538 return rc;
539}
540
561uint64_t
562storage::du(const etl::istring &path)
563{
564 if (!ready()) {
565 throw not_ready_exception(__FILE__, __LINE__);
566 }
567
568 if (path.size() >= CONFIG_STORAGE_MAX_PATH_LEN) {
569 throw max_path_exception(__FILE__, __LINE__);
570 }
571
572 scoped_lock lock(&m_mtx);
573
574 uint64_t total = 0;
575 struct fs_dirent entry;
576
577 /* If the path is a file get the size directly */
578 int rc = fs_stat(path.c_str(), &entry);
579 if (rc < 0) {
580 throw fs_exception(__FILE__, __LINE__);
581 }
582 if (entry.type == FS_DIR_ENTRY_FILE) {
583 return entry.size;
584 }
585
586 m_dfs_dirs.clear();
587 m_dfs_dirs.push(path);
588
589 while (!m_dfs_dirs.empty()) {
590 /*
591 * Get a reference to the path in order to reduce the stack usage as each
592 * path consumes quite a lot of memory. Take care not to pop the reference
593 * contents, until no more is needed.
594 */
595 auto &current = m_dfs_dirs.front();
596 directory dir(current);
597
598 while (ls(dir, entry)) {
599 if (entry.type == FS_DIR_ENTRY_FILE) {
600 total += entry.size;
601 } else if (entry.type == FS_DIR_ENTRY_DIR && m_dfs_dirs.full() == false) {
602 m_dfs_dirs.push(current);
603 auto &new_dir = m_dfs_dirs.back();
604 new_dir += "/";
605 new_dir += entry.name;
606 }
607 }
608 /* Now the path string can be released */
609 m_dfs_dirs.pop();
610 }
611 return total;
612}
613
623void
624storage::ls(const etl::istring &path, etl::istring &res)
625{
626 if (!ready()) {
627 throw not_ready_exception(__FILE__, __LINE__);
628 }
629
630 struct fs_dirent entry;
631 scoped_lock lock(&m_mtx);
632 etl::string_stream stream(m_pname);
633 directory dir(path);
634
635 bool more = true;
636 while (more) {
637 more = ls(dir, entry);
638 if (more) {
639 m_pname.clear();
640 if (entry.type == FS_DIR_ENTRY_DIR) {
641 stream << path.c_str() << "/" << entry.name << "/"
642 << "\n";
643 } else {
644 stream << path.c_str() << "/" << entry.name << "\n";
645 }
646 res.append(m_pname);
647 }
648 }
649}
650
668bool
669storage::ls(directory &dir, fs_dirent &entry)
670{
671 if (!ready()) {
672 throw not_ready_exception(__FILE__, __LINE__);
673 }
674
675 int rc = fs_readdir(&dir.m_dir, &entry);
676 if (rc || entry.name[0] == 0) {
677 return false;
678 }
679 return true;
680}
681
691bool
692storage::is_file(const etl::istring &path)
693{
694 struct fs_dirent entry;
695 int rc = fs_stat(path.c_str(), &entry);
696 if (rc < 0) {
697 throw fs_exception(__FILE__, __LINE__);
698 }
699 return entry.type == FS_DIR_ENTRY_FILE;
700}
701
713bool
714storage::is_dir(const etl::istring &path)
715{
716 return !is_file(path);
717}
718
729size_t
730storage::read(const etl::istring &path, uint8_t *b, size_t len, size_t offset)
731{
732 file f(path);
733 return read(f, b, len, offset);
734}
735
746size_t
747storage::read(file &f, uint8_t *b, size_t len, size_t offset)
748{
749 scoped_lock lock(&m_mtx);
750 fs_seek(&f.m_file, offset, FS_SEEK_SET);
751 len = std::min<size_t>(len, CONFIG_MAX_MTU);
752 ssize_t ret = fs_read(&f.m_file, aligned_buffer, len);
753 if (ret < 0) {
754 throw read_exception(__FILE__, __LINE__);
755 }
756 std::copy_n(aligned_buffer, ret, b);
757 return ret;
758}
759
769size_t
770storage::read(file &f, uint8_t *b, size_t len)
771{
772 scoped_lock lock(&m_mtx);
773 len = std::min<size_t>(len, CONFIG_MAX_MTU);
774 ssize_t ret = fs_read(&f.m_file, aligned_buffer, len);
775 if (ret < 0) {
776 throw read_exception(__FILE__, __LINE__);
777 }
778 std::copy_n(aligned_buffer, ret, b);
779 return ret;
780}
781
795size_t
796storage::write_raw(const uint8_t *b, size_t start_sector, size_t len)
797{
798 if (!ready_raw()) {
799 throw not_ready_exception(__FILE__, __LINE__);
800 }
801
802 /* Get the number of full sectors to write */
803 size_t sectors = len / SECTOR_SIZE;
804 uint32_t remainder = len % SECTOR_SIZE;
805
806 /* Do not touch the littleFS
807 * check if the sectors that need to be written overlap
808 * with the starting sector of the littlefs.
809 * if we have remaining bytes we need +1 sector
810 */
811 if (start_sector + sectors + (remainder ? 1 : 0) >=
812 CONFIG_DISK_START_SECTOR) {
813 return 0;
814 }
815
816 for (size_t i = 0; i < sectors; i++) {
817 /* Copy the the memory aligned buffer */
818 std::copy_n(b + i * SECTOR_SIZE, SECTOR_SIZE, aligned_buffer);
819 int ret = disk_access_write(CONFIG_STM32_SDMMC_DEFAULT_DISK_NAME,
820 aligned_buffer, start_sector + i, 1);
821 if (ret) {
822 return 0;
823 }
824 }
825
826 /* Write any semi-full sector */
827 if (remainder) {
828 int ret = disk_access_read(CONFIG_STM32_SDMMC_DEFAULT_DISK_NAME,
829 aligned_buffer, start_sector + sectors, 1);
830 if (ret) {
831
832 /* We were able to write some stuff so far*/
833 return sectors * SECTOR_SIZE;
834 }
835
836 std::copy_n(b + sectors * SECTOR_SIZE, remainder, aligned_buffer);
837 ret = disk_access_write(CONFIG_STM32_SDMMC_DEFAULT_DISK_NAME,
838 aligned_buffer, start_sector + sectors, 1);
839 if (ret) {
840 return sectors * SECTOR_SIZE;
841 }
842 }
843 return len;
844}
845
854size_t
855storage::read_raw(uint8_t *b, size_t start_sector, size_t sectors)
856{
857 if (!ready_raw()) {
858 throw not_ready_exception(__FILE__, __LINE__);
859 }
860
861 size_t cnt = 0;
862 for (size_t i = 0; i < sectors; i++) {
863 int ret = disk_access_read(CONFIG_STM32_SDMMC_DEFAULT_DISK_NAME,
864 aligned_buffer, start_sector + i, 1);
865 if (ret) {
866 return cnt;
867 }
868 cnt++;
869 std::copy_n(aligned_buffer, SECTOR_SIZE, b + i * SECTOR_SIZE);
870 }
871 return sectors;
872}
873
886void
887storage::erase_sectors(size_t start_sector, size_t sectors)
888{
889 if (!ready_raw()) {
890 throw not_ready_exception(__FILE__, __LINE__);
891 }
892
893 std::fill_n(aligned_buffer, 512, 0xFF);
894 for (size_t i = start_sector; i < start_sector + sectors; i++) {
895 disk_access_write(CONFIG_STM32_SDMMC_DEFAULT_DISK_NAME, aligned_buffer, i,
896 1);
897 }
898}
899
905size_t
907{
908 uint32_t cnt = 0;
909 int ret = disk_access_ioctl(CONFIG_STM32_SDMMC_DEFAULT_DISK_NAME,
910 DISK_IOCTL_GET_SECTOR_COUNT, &cnt);
911 if (ret < 0) {
912 return 0;
913 }
914 return cnt;
915}
916
917} // namespace satnogs::comms
static board & get_instance()
Gets a reference to the single instance of the Board interface class.
Definition board.cpp:66
Implements a scoped lock utilizing the Zephyr mutex.
size_t read(const etl::istring &path, uint8_t *b, size_t len, size_t offset)
Reads from a file starting from specific offset.
Definition storage.cpp:730
void ls(const etl::istring &path, etl::istring &res)
Lists the contents of a directory.
Definition storage.cpp:624
void truncate(const etl::istring &path, size_t len)
Truncate a file by removing bytes from its end.
Definition storage.cpp:445
uint64_t du(const etl::istring &path)
Calculate the disk usage of a file or directory likewise the 'du' command of Linux.
Definition storage.cpp:562
size_t read_raw(uint8_t *b, size_t start_sector, size_t sectors)
Reads directly raw sectors from the eMMC, bypassing any filesystem.
Definition storage.cpp:855
bool enabled() const
Checks if the storage subsystem is enabled.
Definition storage.cpp:183
void rmdir(const etl::istring &path)
Removes a directory and all its contents.
Definition storage.cpp:329
float utilization()
Get eMMC storage utilization of the partition used by the MCU.
Definition storage.cpp:80
void mkdir(const etl::istring &path)
Creates a directory at the specified path.
Definition storage.cpp:248
bool ready() const
Checks if the storage subsystem is ready to accept filesystem operations for the MCU side.
Definition storage.cpp:234
size_t write_raw(const uint8_t *b, size_t start_sector, size_t len)
Writes raw bytes on the eMMC.
Definition storage.cpp:796
lib::emmc::dir get_dir() const
Get the direction of the eMMC.
Definition storage.cpp:218
size_t write(const etl::istring &path, const uint8_t *b, size_t len)
Writes data to a file.
Definition storage.cpp:395
size_t sectors_num() const
Returns the number of total sectors of the eMMC.
Definition storage.cpp:906
void erase_sectors(size_t start_sector, size_t sectors)
Erase a specific number of sectors.
Definition storage.cpp:887
void enable(bool en)
Enable or disable the storage subsystem.
Definition storage.cpp:125
bool is_file(const etl::istring &path)
Check if a path is a file.
Definition storage.cpp:692
bool mounted() const
Checks if the storage subsystem is mounted.
Definition storage.cpp:195
void rm(const etl::istring &path)
Removes a file from the filesystem.
Definition storage.cpp:295
bool is_dir(const etl::istring &path)
Check if a path is a directory.
Definition storage.cpp:714
static constexpr size_t SECTOR_SIZE
Definition storage.hpp:36
void set_dir(lib::emmc::dir d)
Set the direction of the eMMC.
Definition storage.cpp:206
storage(storage const &)=delete
FS_LITTLEFS_DECLARE_CUSTOM_CONFIG(lfs_storage, storage::SECTOR_SIZE, storage::SECTOR_SIZE, storage::SECTOR_SIZE, storage::SECTOR_SIZE, 4 *storage::SECTOR_SIZE)
constexpr off_t offset
Definition settings.cpp:34