diff --git a/firmware/application/CMakeLists.txt b/firmware/application/CMakeLists.txt index fed058137..b03955e15 100644 --- a/firmware/application/CMakeLists.txt +++ b/firmware/application/CMakeLists.txt @@ -203,6 +203,7 @@ set(CPPSRC metadata_file.cpp portapack.cpp usb_serial_shell.cpp + usb_serial_shell_filesystem.cpp usb_serial_event.cpp usb_serial_thread.cpp usb_serial.cpp diff --git a/firmware/application/usb_serial_io.c b/firmware/application/usb_serial_io.c index 48b847cd9..f1d0199c9 100644 --- a/firmware/application/usb_serial_io.c +++ b/firmware/application/usb_serial_io.c @@ -142,3 +142,42 @@ void init_serial_usb_driver(SerialUSBDriver* sdp) { chIQInit(&sdp->iqueue, sdp->ib, SERIAL_BUFFERS_SIZE, NULL, sdp); chOQInit(&sdp->oqueue, sdp->ob, SERIAL_BUFFERS_SIZE, onotify, sdp); } + +// queue handler from ch +static msg_t qwait(GenericQueue* qp, systime_t time) { + if (TIME_IMMEDIATE == time) + return Q_TIMEOUT; + currp->p_u.wtobjp = qp; + queue_insert(currp, &qp->q_waiting); + return chSchGoSleepTimeoutS(THD_STATE_WTQUEUE, time); +} + +// This function fills the output buffer, and sends all data in 1 packet +size_t fillOBuffer(OutputQueue* oqp, const uint8_t* bp, size_t n) { + qnotify_t nfy = oqp->q_notify; + size_t w = 0; + + chDbgCheck(n > 0, "chOQWriteTimeout"); + chSysLock(); + while (TRUE) { + while (chOQIsFullI(oqp)) { + if (qwait((GenericQueue*)oqp, TIME_INFINITE) != Q_OK) { + chSysUnlock(); + return w; + } + } + while (!chOQIsFullI(oqp) && n > 0) { + oqp->q_counter--; + *oqp->q_wrptr++ = *bp++; + if (oqp->q_wrptr >= oqp->q_top) + oqp->q_wrptr = oqp->q_buffer; + w++; + --n; + } + if (nfy) nfy(oqp); + + chSysUnlock(); /* Gives a preemption chance in a controlled point.*/ + if (n == 0) return w; + chSysLock(); + } +} diff --git a/firmware/application/usb_serial_io.h b/firmware/application/usb_serial_io.h index 6f69375af..e7c6905ae 100644 --- a/firmware/application/usb_serial_io.h +++ b/firmware/application/usb_serial_io.h @@ -44,3 +44,11 @@ extern SerialUSBDriver SUSBD1; void init_serial_usb_driver(SerialUSBDriver* sdp); void bulk_out_receive(void); void serial_bulk_transfer_complete(void* user_data, unsigned int bytes_transferred); + +#ifdef __cplusplus +extern "C" { +#endif +size_t fillOBuffer(OutputQueue* oqp, const uint8_t* bp, size_t n); +#ifdef __cplusplus +} +#endif diff --git a/firmware/application/usb_serial_shell.cpp b/firmware/application/usb_serial_shell.cpp index 10cab6628..76835681c 100644 --- a/firmware/application/usb_serial_shell.cpp +++ b/firmware/application/usb_serial_shell.cpp @@ -37,7 +37,6 @@ #include "hackrf_cpld_data.hpp" #include "usb_serial_io.h" -#include "ff.h" #include "chprintf.h" #include "chqueues.h" #include "ui_external_items_menu_loader.hpp" @@ -45,11 +44,10 @@ #include "ui_widget.hpp" #include "ui_navigation.hpp" +#include "usb_serial_shell_filesystem.hpp" #include -#include #include -#include #include #define SHELL_WA_SIZE THD_WA_SIZE(1024 * 3) @@ -60,45 +58,6 @@ static EventDispatcher* getEventDispatcherInstance() { return _eventDispatcherInstance; } -// queue handler from ch -static msg_t qwait(GenericQueue* qp, systime_t time) { - if (TIME_IMMEDIATE == time) - return Q_TIMEOUT; - currp->p_u.wtobjp = qp; - queue_insert(currp, &qp->q_waiting); - return chSchGoSleepTimeoutS(THD_STATE_WTQUEUE, time); -} - -// This function fills the output buffer, and sends all data in 1 packet -static size_t fillOBuffer(OutputQueue* oqp, const uint8_t* bp, size_t n) { - qnotify_t nfy = oqp->q_notify; - size_t w = 0; - - chDbgCheck(n > 0, "chOQWriteTimeout"); - chSysLock(); - while (TRUE) { - while (chOQIsFullI(oqp)) { - if (qwait((GenericQueue*)oqp, TIME_INFINITE) != Q_OK) { - chSysUnlock(); - return w; - } - } - while (!chOQIsFullI(oqp) && n > 0) { - oqp->q_counter--; - *oqp->q_wrptr++ = *bp++; - if (oqp->q_wrptr >= oqp->q_top) - oqp->q_wrptr = oqp->q_buffer; - w++; - --n; - } - if (nfy) nfy(oqp); - - chSysUnlock(); /* Gives a preemption chance in a controlled point.*/ - if (n == 0) return w; - chSysLock(); - } -} - static void cmd_reboot(BaseSequentialStream* chp, int argc, char* argv[]) { (void)chp; (void)argc; @@ -161,11 +120,6 @@ static void cmd_sd_over_usb(BaseSequentialStream* chp, int argc, char* argv[]) { m0_halt(); } -std::filesystem::path path_from_string8(char* path) { - std::wstring_convert, char16_t> conv; - return conv.from_bytes(path); -} - bool strEndsWith(const std::u16string& str, const std::u16string& suffix) { if (str.length() >= suffix.length()) { std::u16string endOfString = str.substr(str.length() - suffix.length()); @@ -432,186 +386,6 @@ static void cmd_keyboard(BaseSequentialStream* chp, int argc, char* argv[]) { chprintf(chp, "ok\r\n"); } -static void cmd_sd_list_dir(BaseSequentialStream* chp, int argc, char* argv[]) { - if (argc != 1) { - chprintf(chp, "usage: ls /\r\n"); - return; - } - - auto path = path_from_string8(argv[0]); - - for (const auto& entry : std::filesystem::directory_iterator(path, "*")) { - if (std::filesystem::is_directory(entry.status())) { - chprintf(chp, "%s/\r\n", entry.path().string().c_str()); - } else if (std::filesystem::is_regular_file(entry.status())) { - chprintf(chp, "%s\r\n", entry.path().string().c_str()); - } else { - chprintf(chp, "%s *\r\n", entry.path().string().c_str()); - } - } -} - -static void cmd_sd_delete(BaseSequentialStream* chp, int argc, char* argv[]) { - if (argc != 1) { - chprintf(chp, "usage: rm \r\n"); - return; - } - - auto path = path_from_string8(argv[0]); - - if (!std::filesystem::file_exists(path)) { - chprintf(chp, "file not found.\r\n"); - return; - } - - delete_file(path); - - chprintf(chp, "ok\r\n"); -} - -File* shell_file = nullptr; - -static void cmd_sd_filesize(BaseSequentialStream* chp, int argc, char* argv[]) { - if (argc != 1) { - chprintf(chp, "usage: filesize \r\n"); - return; - } - auto path = path_from_string8(argv[0]); - FILINFO res; - auto stat = f_stat(path.tchar(), &res); - if (stat == FR_OK) { - chprintf(chp, "%lu\r\n", res.fsize); - chprintf(chp, "ok\r\n"); - } else { - chprintf(chp, "error\r\n"); - } -} - -static void cmd_sd_open(BaseSequentialStream* chp, int argc, char* argv[]) { - if (argc != 1) { - chprintf(chp, "usage: open \r\n"); - return; - } - - if (shell_file != nullptr) { - chprintf(chp, "file already open\r\n"); - return; - } - - auto path = path_from_string8(argv[0]); - shell_file = new File(); - shell_file->open(path, false, true); - - chprintf(chp, "ok\r\n"); -} - -static void cmd_sd_seek(BaseSequentialStream* chp, int argc, char* argv[]) { - if (argc != 1) { - chprintf(chp, "usage: seek \r\n"); - return; - } - - if (shell_file == nullptr) { - chprintf(chp, "no open file\r\n"); - return; - } - - int address = (int)strtol(argv[0], NULL, 10); - shell_file->seek(address); - - chprintf(chp, "ok\r\n"); -} - -static void cmd_sd_close(BaseSequentialStream* chp, int argc, char* argv[]) { - (void)argv; - - if (argc != 0) { - chprintf(chp, "usage: close\r\n"); - return; - } - - if (shell_file == nullptr) { - chprintf(chp, "no open file\r\n"); - return; - } - - delete shell_file; - shell_file = nullptr; - - chprintf(chp, "ok\r\n"); -} - -static void cmd_sd_read(BaseSequentialStream* chp, int argc, char* argv[]) { - if (argc != 1) { - chprintf(chp, "usage: read \r\n"); - return; - } - - if (shell_file == nullptr) { - chprintf(chp, "no open file\r\n"); - return; - } - - int size = (int)strtol(argv[0], NULL, 10); - - uint8_t buffer[62]; - - do { - File::Size bytes_to_read = size > 62 ? 62 : size; - auto bytes_read = shell_file->read(buffer, bytes_to_read); - if (bytes_read.is_error()) { - chprintf(chp, "error %d\r\n", bytes_read.error()); - return; - } - std::string res = to_string_hex_array(buffer, bytes_read.value()); - res += "\r\n"; - fillOBuffer(&((SerialUSBDriver*)chp)->oqueue, (const uint8_t*)res.c_str(), res.size()); - if (bytes_to_read != bytes_read.value()) - return; - - size -= bytes_to_read; - } while (size > 0); - chprintf(chp, "ok\r\n"); -} - -static void cmd_sd_write(BaseSequentialStream* chp, int argc, char* argv[]) { - const char* usage = "usage: write 0123456789ABCDEF\r\n"; - if (argc != 1) { - chprintf(chp, usage); - return; - } - - if (shell_file == nullptr) { - chprintf(chp, "no open file\r\n"); - return; - } - - size_t data_string_len = strlen(argv[0]); - if (data_string_len % 2 != 0) { - chprintf(chp, usage); - return; - } - - for (size_t i = 0; i < data_string_len; i++) { - char c = argv[0][i]; - if ((c < '0' || c > '9') && (c < 'A' || c > 'F')) { - chprintf(chp, usage); - return; - } - } - - char buffer[3] = {0, 0, 0}; - - for (size_t i = 0; i < data_string_len / 2; i++) { - buffer[0] = argv[0][i * 2]; - buffer[1] = argv[0][i * 2 + 1]; - uint8_t value = (uint8_t)strtol(buffer, NULL, 16); - shell_file->write(&value, 1); - } - - chprintf(chp, "ok\r\n"); -} - static void cmd_rtcget(BaseSequentialStream* chp, int argc, char* argv[]) { (void)chp; (void)argc; @@ -1118,14 +892,7 @@ static const ShellCommand commands[] = { {"button", cmd_button}, {"touch", cmd_touch}, {"keyboard", cmd_keyboard}, - {"ls", cmd_sd_list_dir}, - {"rm", cmd_sd_delete}, - {"open", cmd_sd_open}, - {"seek", cmd_sd_seek}, - {"close", cmd_sd_close}, - {"read", cmd_sd_read}, - {"write", cmd_sd_write}, - {"filesize", cmd_sd_filesize}, + USB_SERIAL_SHELL_SD_COMMANDS, {"rtcget", cmd_rtcget}, {"rtcset", cmd_rtcset}, {"cpld_info", cpld_info}, diff --git a/firmware/application/usb_serial_shell_filesystem.cpp b/firmware/application/usb_serial_shell_filesystem.cpp new file mode 100644 index 000000000..0c35fe82a --- /dev/null +++ b/firmware/application/usb_serial_shell_filesystem.cpp @@ -0,0 +1,366 @@ +/* + * Copyright (C) 2023 Bernd Herzog + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "usb_serial_shell_filesystem.hpp" +#include "usb_serial_io.h" + +#include "chprintf.h" +#include "string_format.hpp" +#include + +static File* shell_file = nullptr; + +static bool report_on_error(BaseSequentialStream* chp, File::Error& error) { + if (error.ok() == false) { + chprintf(chp, "Error calling delete_file: %d %s\r\n", error.code(), error.what().c_str()); + return true; + } + + return false; +} + +static bool report_on_error(BaseSequentialStream* chp, FRESULT error_code) { + std::filesystem::filesystem_error error = error_code; + return report_on_error(chp, error); +} + +static bool report_on_error(BaseSequentialStream* chp, Optional& error) { + if (error.is_valid()) + return report_on_error(chp, error.value()); + + return false; +} + +static bool report_on_error(BaseSequentialStream* chp, File::Result error) { + if (error.is_error()) { + return report_on_error(chp, error.error()); + } + + return false; +} + +void cmd_sd_list_dir(BaseSequentialStream* chp, int argc, char* argv[]) { + if (argc != 1) { + chprintf(chp, "usage: ls /\r\n"); + return; + } + + auto path = path_from_string8(argv[0]); + + for (const auto& entry : std::filesystem::directory_iterator(path, "*")) { + if (std::filesystem::is_directory(entry.status())) { + chprintf(chp, "%s/\r\n", entry.path().string().c_str()); + } else if (std::filesystem::is_regular_file(entry.status())) { + chprintf(chp, "%s\r\n", entry.path().string().c_str()); + } else { + chprintf(chp, "%s *\r\n", entry.path().string().c_str()); + } + } +} + +void cmd_sd_unlink(BaseSequentialStream* chp, int argc, char* argv[]) { + if (argc != 1) { + chprintf(chp, "usage: unlink \r\n"); + return; + } + + auto path = path_from_string8(argv[0]); + auto error = delete_file(path); + if (report_on_error(chp, error)) return; + + chprintf(chp, "ok\r\n"); +} + +void cmd_sd_mkdir(BaseSequentialStream* chp, int argc, char* argv[]) { + if (argc != 1) { + chprintf(chp, "usage: mkdir \r\n"); + return; + } + + auto path = path_from_string8(argv[0]); + + if (!std::filesystem::is_directory(path)) { + chprintf(chp, "directory already exists.\r\n"); + return; + } + + auto error = make_new_directory(path); + if (report_on_error(chp, error)) return; + + chprintf(chp, "ok\r\n"); +} + +void cmd_sd_filesize(BaseSequentialStream* chp, int argc, char* argv[]) { + if (argc != 1) { + chprintf(chp, "usage: filesize \r\n"); + return; + } + auto path = path_from_string8(argv[0]); + FILINFO res; + auto stat = f_stat(path.tchar(), &res); + if (report_on_error(chp, stat)) return; + + chprintf(chp, "%lu\r\n", res.fsize); + chprintf(chp, "ok\r\n"); +} + +void cmd_sd_open(BaseSequentialStream* chp, int argc, char* argv[]) { + if (argc != 1) { + chprintf(chp, "usage: fopen \r\n"); + return; + } + + if (shell_file != nullptr) { + chprintf(chp, "file already open\r\n"); + return; + } + + auto path = path_from_string8(argv[0]); + shell_file = new File(); + auto error = shell_file->open(path, false, true); + if (report_on_error(chp, error)) return; + + chprintf(chp, "ok\r\n"); +} + +void cmd_sd_seek(BaseSequentialStream* chp, int argc, char* argv[]) { + if (argc != 1) { + chprintf(chp, "usage: fseek \r\n"); + return; + } + + if (shell_file == nullptr) { + chprintf(chp, "no open file\r\n"); + return; + } + + int address = (int)strtol(argv[0], NULL, 10); + auto error = shell_file->seek(address); + if (report_on_error(chp, error)) return; + + chprintf(chp, "ok\r\n"); +} + +void cmd_sd_close(BaseSequentialStream* chp, int argc, char* argv[]) { + (void)argv; + + if (argc != 0) { + chprintf(chp, "usage: fclose\r\n"); + return; + } + + if (shell_file == nullptr) { + chprintf(chp, "no open file\r\n"); + return; + } + + delete shell_file; + shell_file = nullptr; + + chprintf(chp, "ok\r\n"); +} + +void cmd_sd_truncate(BaseSequentialStream* chp, int argc, char* argv[]) { + (void)argv; + + if (argc != 0) { + chprintf(chp, "usage: ftruncate\r\n"); + return; + } + + if (shell_file == nullptr) { + chprintf(chp, "no open file\r\n"); + return; + } + + auto error = shell_file->truncate(); + if (report_on_error(chp, error)) return; + + chprintf(chp, "ok\r\n"); +} + +void cmd_sd_sync(BaseSequentialStream* chp, int argc, char* argv[]) { + (void)argv; + + if (argc != 0) { + chprintf(chp, "usage: fsync\r\n"); + return; + } + + if (shell_file == nullptr) { + chprintf(chp, "no open file\r\n"); + return; + } + + auto error = shell_file->sync(); + if (report_on_error(chp, error)) return; + + chprintf(chp, "ok\r\n"); +} + +void cmd_sd_tell(BaseSequentialStream* chp, int argc, char* argv[]) { + (void)argv; + + if (argc != 0) { + chprintf(chp, "usage: ftell\r\n"); + return; + } + + if (shell_file == nullptr) { + chprintf(chp, "no open file\r\n"); + return; + } + + auto current_position = shell_file->tell(); + + chprintf(chp, "%d\r\n", current_position); + chprintf(chp, "ok\r\n"); +} + +void cmd_sd_read(BaseSequentialStream* chp, int argc, char* argv[]) { + if (argc != 1) { + chprintf(chp, "usage: fread \r\n"); + return; + } + + if (shell_file == nullptr) { + chprintf(chp, "no open file\r\n"); + return; + } + + int size = (int)strtol(argv[0], NULL, 10); + + uint8_t buffer[62]; + + do { + File::Size bytes_to_read = size > 62 ? 62 : size; + auto bytes_read = shell_file->read(buffer, bytes_to_read); + if (report_on_error(chp, bytes_read)) return; + + std::string res = to_string_hex_array(buffer, bytes_read.value()); + res += "\r\n"; + fillOBuffer(&((SerialUSBDriver*)chp)->oqueue, (const uint8_t*)res.c_str(), res.size()); + if (bytes_to_read != bytes_read.value()) + return; + + size -= bytes_to_read; + } while (size > 0); + chprintf(chp, "ok\r\n"); +} + +void cmd_sd_read_binary(BaseSequentialStream* chp, int argc, char* argv[]) { + if (argc != 1) { + chprintf(chp, "usage: frb \r\n"); + return; + } + + if (shell_file == nullptr) { + chprintf(chp, "no open file\r\n"); + return; + } + + int size = (int)strtol(argv[0], NULL, 10); + + uint8_t buffer[64]; + + do { + File::Size bytes_to_read = size > 64 ? 64 : size; + auto bytes_read = shell_file->read(buffer, bytes_to_read); + if (report_on_error(chp, bytes_read)) return; + + if (bytes_read.value() > 0) + fillOBuffer(&((SerialUSBDriver*)chp)->oqueue, buffer, bytes_read.value()); + + if (bytes_to_read != bytes_read.value()) + return; + + size -= bytes_to_read; + } while (size > 0); + chprintf(chp, "\r\nok\r\n"); +} + +void cmd_sd_write(BaseSequentialStream* chp, int argc, char* argv[]) { + const char* usage = "usage: fwrite 0123456789ABCDEF\r\n"; + if (argc != 1) { + chprintf(chp, usage); + return; + } + + if (shell_file == nullptr) { + chprintf(chp, "no open file\r\n"); + return; + } + + size_t data_string_len = strlen(argv[0]); + if (data_string_len % 2 != 0) { + chprintf(chp, usage); + return; + } + + for (size_t i = 0; i < data_string_len; i++) { + char c = argv[0][i]; + if ((c < '0' || c > '9') && (c < 'A' || c > 'F')) { + chprintf(chp, usage); + return; + } + } + + char buffer[3] = {0, 0, 0}; + + for (size_t i = 0; i < data_string_len / 2; i++) { + buffer[0] = argv[0][i * 2]; + buffer[1] = argv[0][i * 2 + 1]; + uint8_t value = (uint8_t)strtol(buffer, NULL, 16); + auto error = shell_file->write(&value, 1); + if (report_on_error(chp, error)) return; + } + + chprintf(chp, "ok\r\n"); +} + +void cmd_sd_write_binary(BaseSequentialStream* chp, int argc, char* argv[]) { + const char* usage = "usage: fwb \r\nfollowed by of data"; + if (argc != 1) { + chprintf(chp, usage); + return; + } + + if (shell_file == nullptr) { + chprintf(chp, "no open file\r\n"); + return; + } + + long size = (int)strtol(argv[0], NULL, 10); + + chprintf(chp, "send %d bytes\r\n", size); + + uint8_t buffer; + + for (long i = 0; i < size; i++) { + if (chSequentialStreamRead(chp, &buffer, 1) == 0) + return; + + auto error = shell_file->write(&buffer, 1); + if (report_on_error(chp, error)) return; + } + + chprintf(chp, "ok\r\n"); +} diff --git a/firmware/application/usb_serial_shell_filesystem.hpp b/firmware/application/usb_serial_shell_filesystem.hpp new file mode 100644 index 000000000..af21d155b --- /dev/null +++ b/firmware/application/usb_serial_shell_filesystem.hpp @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2023 Bernd Herzog + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "ch.h" +#include "hal.h" + +#include +#include + +#include "ff.h" +#include "file.hpp" + +void cmd_sd_list_dir(BaseSequentialStream* chp, int argc, char* argv[]); +void cmd_sd_unlink(BaseSequentialStream* chp, int argc, char* argv[]); +void cmd_sd_mkdir(BaseSequentialStream* chp, int argc, char* argv[]); +void cmd_sd_filesize(BaseSequentialStream* chp, int argc, char* argv[]); +void cmd_sd_open(BaseSequentialStream* chp, int argc, char* argv[]); +void cmd_sd_seek(BaseSequentialStream* chp, int argc, char* argv[]); +void cmd_sd_close(BaseSequentialStream* chp, int argc, char* argv[]); +void cmd_sd_truncate(BaseSequentialStream* chp, int argc, char* argv[]); +void cmd_sd_sync(BaseSequentialStream* chp, int argc, char* argv[]); +void cmd_sd_tell(BaseSequentialStream* chp, int argc, char* argv[]); +void cmd_sd_read(BaseSequentialStream* chp, int argc, char* argv[]); +void cmd_sd_read_binary(BaseSequentialStream* chp, int argc, char* argv[]); +void cmd_sd_write(BaseSequentialStream* chp, int argc, char* argv[]); +void cmd_sd_write_binary(BaseSequentialStream* chp, int argc, char* argv[]); + +static std::filesystem::path path_from_string8(char* path) { + std::wstring_convert, char16_t> conv; + return conv.from_bytes(path); +} + +// clang-format off +#define USB_SERIAL_SHELL_SD_COMMANDS \ + {"ls", cmd_sd_list_dir}, \ + {"unlink", cmd_sd_unlink}, \ + {"mkdir", cmd_sd_mkdir}, \ + {"filesize", cmd_sd_filesize}, \ + {"fopen", cmd_sd_open}, \ + {"fseek", cmd_sd_seek}, \ + {"fclose", cmd_sd_close}, \ + {"ftruncate", cmd_sd_truncate}, \ + {"fsync", cmd_sd_sync}, \ + {"ftell", cmd_sd_tell}, \ + {"fread", cmd_sd_read}, \ + {"frb", cmd_sd_read_binary}, \ + {"fread", cmd_sd_write}, \ + {"fwb", cmd_sd_write_binary} +// clang-format on