Cosmetics
[opentx.git] / radio / src / sdcard.cpp
blob3a16fde9f6a2828af76c74004c0ea2c7e4df32e8
1 /*
2 * Copyright (C) OpenTX
4 * Based on code named
5 * th9x - http://code.google.com/p/th9x
6 * er9x - http://code.google.com/p/er9x
7 * gruvin9x - http://code.google.com/p/gruvin9x
9 * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html
11 * This program is free software; you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License version 2 as
13 * published by the Free Software Foundation.
15 * This program is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 * GNU General Public License for more details.
21 #include <stdint.h>
22 #include "opentx.h"
23 #include "diskio.h"
25 bool sdCardFormat()
27 BYTE work[_MAX_SS];
28 FRESULT res = f_mkfs("", FM_FAT32, 0, work, sizeof(work));
29 switch(res) {
30 case FR_OK :
31 return true;
32 case FR_DISK_ERR:
33 POPUP_WARNING("Format error");
34 return false;
35 case FR_NOT_READY:
36 POPUP_WARNING("SDCard not ready");
37 return false;
38 case FR_WRITE_PROTECTED:
39 POPUP_WARNING("SDCard write protected");
40 return false;
41 case FR_INVALID_PARAMETER:
42 POPUP_WARNING("Format param invalid");
43 return false;
44 case FR_INVALID_DRIVE:
45 POPUP_WARNING("Invalid drive");
46 return false;
47 case FR_MKFS_ABORTED:
48 POPUP_WARNING("Format aborted");
49 return false;
50 default:
51 POPUP_WARNING(STR_SDCARD_ERROR);
52 return false;
56 const char * sdCheckAndCreateDirectory(const char * path)
58 DIR archiveFolder;
60 FRESULT result = f_opendir(&archiveFolder, path);
61 if (result != FR_OK) {
62 if (result == FR_NO_PATH)
63 result = f_mkdir(path);
64 if (result != FR_OK)
65 return SDCARD_ERROR(result);
67 else {
68 f_closedir(&archiveFolder);
71 return nullptr;
74 bool isFileAvailable(const char * path, bool exclDir)
76 if (exclDir) {
77 FILINFO fno;
78 return (f_stat(path, &fno) == FR_OK && !(fno.fattrib & AM_DIR));
80 return f_stat(path, nullptr) == FR_OK;
83 /**
84 Search file system path for a file. Can optionally take a list of file extensions to search through.
85 Eg. find "splash.bmp", or the first occurrence of one of "splash.[bmp|jpeg|jpg|gif]".
87 @param path String with path name, no trailing slash. eg; "/BITMAPS"
88 @param file String containing file name to search for, with or w/out an extension.
89 eg; "splash.bmp" or "splash"
90 @param pattern Optional list of one or more file extensions concatenated together, including the period(s).
91 The list is searched backwards and the first match, if any, is returned. If null, then only the actual filename
92 passed will be searched for.
93 eg: ".gif.jpg.jpeg.bmp"
94 @param exclDir true/false whether to allow directory matches (default true, excludes directories)
95 @param match Optional container to hold the matched file extension (wide enough to hold LEN_FILE_EXTENSION_MAX + 1).
96 @retval true if a file was found, false otherwise.
98 bool isFilePatternAvailable(const char * path, const char * file, const char * pattern = nullptr, bool exclDir = true, char * match = nullptr)
100 uint8_t fplen;
101 char fqfp[LEN_FILE_PATH_MAX + _MAX_LFN + 1] = "\0";
103 fplen = strlen(path);
104 if (fplen > LEN_FILE_PATH_MAX) {
105 TRACE_ERROR("isFilePatternAvailable(%s) = error: file path too long.\n", path, file);
106 return false;
109 strcpy(fqfp, path);
110 strcpy(fqfp + fplen, "/");
111 strncat(fqfp + (++fplen), file, _MAX_LFN);
113 if (pattern == nullptr) {
114 // no extensions list, just check the filename as-is
115 return isFileAvailable(fqfp, exclDir);
117 else {
118 // extensions list search
119 const char *ext;
120 uint16_t len;
121 uint8_t extlen, fnlen;
122 int plen;
124 getFileExtension(file, 0, 0, &fnlen, &extlen);
125 len = fplen + fnlen - extlen;
126 fqfp[len] = '\0';
127 ext = getFileExtension(pattern, 0, 0, &fnlen, &extlen);
128 plen = (int)fnlen;
129 while (plen > 0 && ext) {
130 strncat(fqfp + len, ext, extlen);
131 if (isFileAvailable(fqfp, exclDir)) {
132 if (match != nullptr) strncat(&(match[0]='\0'), ext, extlen);
133 return true;
135 plen -= extlen;
136 if (plen > 0) {
137 fqfp[len] = '\0';
138 ext = getFileExtension(pattern, plen, 0, nullptr, &extlen);
142 return false;
145 char * getFileIndex(char * filename, unsigned int & value)
147 value = 0;
148 char * pos = (char *)getFileExtension(filename);
149 if (!pos || pos == filename)
150 return nullptr;
151 int multiplier = 1;
152 while (pos > filename) {
153 pos--;
154 char c = *pos;
155 if (c >= '0' && c <= '9') {
156 value += multiplier * (c - '0');
157 multiplier *= 10;
159 else {
160 return pos+1;
163 return filename;
166 uint8_t getDigitsCount(unsigned int value)
168 uint8_t count = 1;
169 while (value >= 10) {
170 value /= 10;
171 ++count;
173 return count;
176 int findNextFileIndex(char * filename, uint8_t size, const char * directory)
178 unsigned int index;
179 uint8_t extlen;
180 char * indexPos = getFileIndex(filename, index);
181 char extension[LEN_FILE_EXTENSION_MAX+1] = "\0";
182 char * p = (char *)getFileExtension(filename, 0, 0, nullptr, &extlen);
183 if (p) strncat(extension, p, sizeof(extension)-1);
184 while (1) {
185 index++;
186 if ((indexPos - filename) + getDigitsCount(index) + extlen > size) {
187 return 0;
189 char * pos = strAppendUnsigned(indexPos, index);
190 strAppend(pos, extension);
191 if (!isFilePatternAvailable(directory, filename, nullptr, false)) {
192 return index;
197 const char * getBasename(const char * path)
199 for (int8_t i = strlen(path) - 1; i >= 0; i--) {
200 if (path[i] == '/') {
201 return &path[i + 1];
204 return path;
207 const char * getFileExtension(const char * filename, uint8_t size, uint8_t extMaxLen, uint8_t *fnlen, uint8_t *extlen)
209 int len = size;
210 if (!size) {
211 len = strlen(filename);
213 if (!extMaxLen) {
214 extMaxLen = LEN_FILE_EXTENSION_MAX;
216 if (fnlen != nullptr) {
217 *fnlen = (uint8_t)len;
219 for (int i=len-1; i >= 0 && len-i <= extMaxLen; --i) {
220 if (filename[i] == '.') {
221 if (extlen) {
222 *extlen = len-i;
224 return &filename[i];
227 if (extlen != nullptr) {
228 *extlen = 0;
230 return nullptr;
234 Check if given extension exists in a list of extensions.
235 @param extension The extension to search for, including leading period.
236 @param pattern One or more file extensions concatenated together, including the periods.
237 The list is searched backwards and the first match, if any, is returned.
238 eg: ".gif.jpg.jpeg.png"
239 @param match Optional container to hold the matched file extension (wide enough to hold LEN_FILE_EXTENSION_MAX + 1).
240 @retval true if a extension was found in the lost, false otherwise.
242 bool isExtensionMatching(const char * extension, const char * pattern, char * match)
244 const char *ext;
245 uint8_t extlen, fnlen;
246 int plen;
248 ext = getFileExtension(pattern, 0, 0, &fnlen, &extlen);
249 plen = (int)fnlen;
250 while (plen > 0 && ext) {
251 if (!strncasecmp(extension, ext, extlen)) {
252 if (match != nullptr) strncat(&(match[0]='\0'), ext, extlen);
253 return true;
255 plen -= extlen;
256 if (plen > 0) {
257 ext = getFileExtension(pattern, plen, 0, nullptr, &extlen);
260 return false;
263 bool sdListFiles(const char * path, const char * extension, const uint8_t maxlen, const char * selection, uint8_t flags)
265 static uint16_t lastpopupMenuOffset = 0;
266 FILINFO fno;
267 DIR dir;
268 const char * fnExt;
269 uint8_t fnLen, extLen;
270 char tmpExt[LEN_FILE_EXTENSION_MAX+1] = "\0";
272 popupMenuOffsetType = MENU_OFFSET_EXTERNAL;
274 static uint8_t s_last_flags;
276 if (selection) {
277 s_last_flags = flags;
278 if (!isFilePatternAvailable(path, selection, ((flags & LIST_SD_FILE_EXT) ? nullptr : extension))) selection = nullptr;
280 else {
281 flags = s_last_flags;
284 if (popupMenuOffset == 0) {
285 lastpopupMenuOffset = 0;
286 memset(reusableBuffer.modelsel.menu_bss, 0, sizeof(reusableBuffer.modelsel.menu_bss));
288 else if (popupMenuOffset == popupMenuItemsCount - MENU_MAX_DISPLAY_LINES) {
289 lastpopupMenuOffset = 0xffff;
290 memset(reusableBuffer.modelsel.menu_bss, 0, sizeof(reusableBuffer.modelsel.menu_bss));
292 else if (popupMenuOffset == lastpopupMenuOffset) {
293 // should not happen, only there because of Murphy's law
294 return true;
296 else if (popupMenuOffset > lastpopupMenuOffset) {
297 memmove(reusableBuffer.modelsel.menu_bss[0], reusableBuffer.modelsel.menu_bss[1], (MENU_MAX_DISPLAY_LINES-1)*MENU_LINE_LENGTH);
298 memset(reusableBuffer.modelsel.menu_bss[MENU_MAX_DISPLAY_LINES-1], 0xff, MENU_LINE_LENGTH);
300 else {
301 memmove(reusableBuffer.modelsel.menu_bss[1], reusableBuffer.modelsel.menu_bss[0], (MENU_MAX_DISPLAY_LINES-1)*MENU_LINE_LENGTH);
302 memset(reusableBuffer.modelsel.menu_bss[0], 0, MENU_LINE_LENGTH);
305 popupMenuItemsCount = 0;
307 FRESULT res = f_opendir(&dir, path);
308 if (res == FR_OK) {
310 if (flags & LIST_NONE_SD_FILE) {
311 popupMenuItemsCount++;
312 if (selection) {
313 lastpopupMenuOffset++;
315 else if (popupMenuOffset==0 || popupMenuOffset < lastpopupMenuOffset) {
316 char * line = reusableBuffer.modelsel.menu_bss[0];
317 memset(line, 0, MENU_LINE_LENGTH);
318 strcpy(line, "---");
319 popupMenuItems[0] = line;
323 for (;;) {
324 res = f_readdir(&dir, &fno); /* Read a directory item */
325 if (res != FR_OK || fno.fname[0] == 0) break; /* Break on error or end of dir */
326 if (fno.fattrib & AM_DIR) continue; /* Skip subfolders */
327 if (fno.fattrib & AM_HID) continue; /* Skip hidden files */
328 if (fno.fattrib & AM_SYS) continue; /* Skip system files */
330 fnExt = getFileExtension(fno.fname, 0, 0, &fnLen, &extLen);
331 fnLen -= extLen;
333 // TRACE_DEBUG("listSdFiles(%s, %s, %u, %s, %u): fn='%s'; fnExt='%s'; match=%d\n",
334 // path, extension, maxlen, (selection ? selection : "nul"), flags, fno.fname, (fnExt ? fnExt : "nul"), (fnExt && isExtensionMatching(fnExt, extension)));
335 // file validation checks
336 if (!fnLen || fnLen > maxlen || ( // wrong size
337 fnExt && extension && ( // extension-based checks follow...
338 !isExtensionMatching(fnExt, extension) || ( // wrong extension
339 !(flags & LIST_SD_FILE_EXT) && // only if we want unique file names...
340 strcasecmp(fnExt, getFileExtension(extension)) && // possible duplicate file name...
341 isFilePatternAvailable(path, fno.fname, extension, true, tmpExt) && // find the first file from extensions list...
342 strncasecmp(fnExt, tmpExt, LEN_FILE_EXTENSION_MAX) // found file doesn't match, this is a duplicate
347 continue;
350 popupMenuItemsCount++;
352 if (!(flags & LIST_SD_FILE_EXT)) {
353 fno.fname[fnLen] = '\0'; // strip extension
356 if (popupMenuOffset == 0) {
357 if (selection && strncasecmp(fno.fname, selection, maxlen) < 0) {
358 lastpopupMenuOffset++;
360 else {
361 for (uint8_t i=0; i<MENU_MAX_DISPLAY_LINES; i++) {
362 char * line = reusableBuffer.modelsel.menu_bss[i];
363 if (line[0] == '\0' || strcasecmp(fno.fname, line) < 0) {
364 if (i < MENU_MAX_DISPLAY_LINES-1) memmove(reusableBuffer.modelsel.menu_bss[i+1], line, sizeof(reusableBuffer.modelsel.menu_bss[i]) * (MENU_MAX_DISPLAY_LINES-1-i));
365 memset(line, 0, MENU_LINE_LENGTH);
366 strcpy(line, fno.fname);
367 break;
371 for (uint8_t i=0; i<min(popupMenuItemsCount, (uint16_t)MENU_MAX_DISPLAY_LINES); i++) {
372 popupMenuItems[i] = reusableBuffer.modelsel.menu_bss[i];
375 else if (lastpopupMenuOffset == 0xffff) {
376 for (int i=MENU_MAX_DISPLAY_LINES-1; i>=0; i--) {
377 char * line = reusableBuffer.modelsel.menu_bss[i];
378 if (line[0] == '\0' || strcasecmp(fno.fname, line) > 0) {
379 if (i > 0) memmove(reusableBuffer.modelsel.menu_bss[0], reusableBuffer.modelsel.menu_bss[1], sizeof(reusableBuffer.modelsel.menu_bss[i]) * i);
380 memset(line, 0, MENU_LINE_LENGTH);
381 strcpy(line, fno.fname);
382 break;
385 for (uint8_t i=0; i<min(popupMenuItemsCount, (uint16_t)MENU_MAX_DISPLAY_LINES); i++) {
386 popupMenuItems[i] = reusableBuffer.modelsel.menu_bss[i];
389 else if (popupMenuOffset > lastpopupMenuOffset) {
390 if (strcasecmp(fno.fname, reusableBuffer.modelsel.menu_bss[MENU_MAX_DISPLAY_LINES-2]) > 0 && strcasecmp(fno.fname, reusableBuffer.modelsel.menu_bss[MENU_MAX_DISPLAY_LINES-1]) < 0) {
391 memset(reusableBuffer.modelsel.menu_bss[MENU_MAX_DISPLAY_LINES-1], 0, MENU_LINE_LENGTH);
392 strcpy(reusableBuffer.modelsel.menu_bss[MENU_MAX_DISPLAY_LINES-1], fno.fname);
395 else {
396 if (strcasecmp(fno.fname, reusableBuffer.modelsel.menu_bss[1]) < 0 && strcasecmp(fno.fname, reusableBuffer.modelsel.menu_bss[0]) > 0) {
397 memset(reusableBuffer.modelsel.menu_bss[0], 0, MENU_LINE_LENGTH);
398 strcpy(reusableBuffer.modelsel.menu_bss[0], fno.fname);
402 f_closedir(&dir);
405 if (popupMenuOffset > 0)
406 lastpopupMenuOffset = popupMenuOffset;
407 else
408 popupMenuOffset = lastpopupMenuOffset;
410 return popupMenuItemsCount;
413 constexpr uint32_t TEXT_FILE_MAXSIZE = 2048;
415 void sdReadTextFile(const char * filename, char lines[NUM_BODY_LINES][LCD_COLS + 1], int & lines_count)
417 FIL file;
418 int result;
419 char c;
420 unsigned int sz;
421 int line_length = 0;
422 uint8_t escape = 0;
423 char escape_chars[4] = {0};
424 int current_line = 0;
426 memclear(lines, NUM_BODY_LINES * (LCD_COLS + 1));
428 result = f_open(&file, filename, FA_OPEN_EXISTING | FA_READ);
429 if (result == FR_OK) {
430 for (unsigned i = 0; i < TEXT_FILE_MAXSIZE && f_read(&file, &c, 1, &sz) == FR_OK && sz == 1 && (lines_count == 0 || current_line - menuVerticalOffset < NUM_BODY_LINES); i++) {
431 if (c == '\n') {
432 ++current_line;
433 line_length = 0;
434 escape = 0;
436 else if (c!='\r' && current_line>=menuVerticalOffset && current_line-menuVerticalOffset<NUM_BODY_LINES && line_length<LCD_COLS) {
437 if (c == '\\' && escape == 0) {
438 escape = 1;
439 continue;
441 else if (c != '\\' && escape > 0 && escape < sizeof(escape_chars)) {
442 escape_chars[escape - 1] = c;
443 if (escape == 2 && !strncmp(escape_chars, "up", 2)) {
444 c = '\300';
446 else if (escape == 2 && !strncmp(escape_chars, "dn", 2)) {
447 c = '\301';
449 else if (escape == 3) {
450 int val = atoi(escape_chars);
451 if (val >= 200 && val < 225) {
452 c = '\200' + val-200;
455 else {
456 escape++;
457 continue;
460 else if (c=='~') {
461 c = 'z'+1;
463 else if (c=='\t') {
464 c = 0x1D; //tab
466 escape = 0;
467 lines[current_line-menuVerticalOffset][line_length++] = c;
470 if (c != '\n') {
471 current_line += 1;
473 f_close(&file);
476 if (lines_count == 0) {
477 lines_count = current_line;
481 // returns true if current working dir is at the root level
482 bool isCwdAtRoot()
484 char path[10];
485 if (f_getcwd(path, sizeof(path)-1) == FR_OK) {
486 return (strcasecmp("/", path) == 0);
488 return false;
492 Wrapper around the f_readdir() function which
493 also returns ".." entry for sub-dirs. (FatFS 0.12 does
494 not return ".", ".." dirs anymore)
496 FRESULT sdReadDir(DIR * dir, FILINFO * fno, bool & firstTime)
498 FRESULT res;
499 if (firstTime && !isCwdAtRoot()) {
500 // fake parent directory entry
501 strcpy(fno->fname, "..");
502 fno->fattrib = AM_DIR;
503 res = FR_OK;
505 else {
506 res = f_readdir(dir, fno); /* Read a directory item */
508 firstTime = false;
509 return res;
512 #if defined(SDCARD)
513 const char * sdCopyFile(const char * srcPath, const char * destPath)
515 FIL srcFile;
516 FIL destFile;
517 char buf[256];
518 UINT read = sizeof(buf);
519 UINT written = sizeof(buf);
521 FRESULT result = f_open(&srcFile, srcPath, FA_OPEN_EXISTING | FA_READ);
522 if (result != FR_OK) {
523 return SDCARD_ERROR(result);
526 result = f_open(&destFile, destPath, FA_CREATE_ALWAYS | FA_WRITE);
527 if (result != FR_OK) {
528 f_close(&srcFile);
529 return SDCARD_ERROR(result);
532 while (result==FR_OK && read==sizeof(buf) && written==sizeof(buf)) {
533 result = f_read(&srcFile, buf, sizeof(buf), &read);
534 if (result == FR_OK) {
535 result = f_write(&destFile, buf, read, &written);
539 f_close(&destFile);
540 f_close(&srcFile);
542 if (result != FR_OK) {
543 return SDCARD_ERROR(result);
546 return nullptr;
549 const char * sdCopyFile(const char * srcFilename, const char * srcDir, const char * destFilename, const char * destDir)
551 char srcPath[2*CLIPBOARD_PATH_LEN+1];
552 char * tmp = strAppend(srcPath, srcDir, CLIPBOARD_PATH_LEN);
553 *tmp++ = '/';
554 strAppend(tmp, srcFilename, CLIPBOARD_PATH_LEN);
556 char destPath[2*CLIPBOARD_PATH_LEN+1];
557 tmp = strAppend(destPath, destDir, CLIPBOARD_PATH_LEN);
558 *tmp++ = '/';
559 strAppend(tmp, destFilename, CLIPBOARD_PATH_LEN);
561 return sdCopyFile(srcPath, destPath);
563 #endif // defined(SDCARD)
566 #if !defined(SIMU) || defined(SIMU_DISKIO)
567 uint32_t sdGetNoSectors()
569 static DWORD noSectors = 0;
570 if (noSectors == 0 ) {
571 disk_ioctl(0, GET_SECTOR_COUNT, &noSectors);
573 return noSectors;
576 uint32_t sdGetSize()
578 return (sdGetNoSectors() / 1000000) * BLOCK_SIZE;
581 uint32_t sdGetFreeSectors()
583 DWORD nofree;
584 FATFS * fat;
585 if (f_getfree("", &nofree, &fat) != FR_OK) {
586 return 0;
588 return nofree * fat->csize;
591 #else // #if !defined(SIMU) || defined(SIMU_DISKIO)
593 uint32_t sdGetNoSectors()
595 return 0;
598 uint32_t sdGetSize()
600 return 0;
603 uint32_t sdGetFreeSectors()
605 return 10;
608 #endif // #if !defined(SIMU) || defined(SIMU_DISKIO)