diff --git a/firmware/keira/platformio.ini b/firmware/keira/platformio.ini index 411d95c1..2d427d4c 100644 --- a/firmware/keira/platformio.ini +++ b/firmware/keira/platformio.ini @@ -28,6 +28,7 @@ lib_deps = https://github.com/moononournation/arduino-nofrendo.git bitbank2/PNGenc @ ^1.1.1 bblanchon/ArduinoJson @ ^7.0.4 + lennarthennigs/ESP Telnet @ ^2.2.1 https://github.com/earlephilhower/ESP8266Audio.git lib_extra_dirs = ./lib extra_scripts = targets.py diff --git a/firmware/keira/src/apps/launcher.cpp b/firmware/keira/src/apps/launcher.cpp index 60f1a740..c670f491 100644 --- a/firmware/keira/src/apps/launcher.cpp +++ b/firmware/keira/src/apps/launcher.cpp @@ -203,7 +203,7 @@ void LauncherApp::sdBrowserMenu(FS* fSysDriver, const String& path) { uint16_t color = entries[i].type == lilka::EntryType::ENT_DIRECTORY ? lilka::colors::Arylide_yellow : get_file_color(filename); if (entries[i].type != lilka::EntryType::ENT_DIRECTORY) - menu.addItem(filename, icon, color, lilka::fileutils.getHumanFriendlySize(entries[i].size)); + menu.addItem(filename, icon, color, lilka::fileutils.getHumanFriendlySize(entries[i].size, true)); else menu.addItem(filename, icon, color); } menu.addItem("<< Назад", 0, 0); diff --git a/firmware/keira/src/main.cpp b/firmware/keira/src/main.cpp index f973c911..cff13ee7 100644 --- a/firmware/keira/src/main.cpp +++ b/firmware/keira/src/main.cpp @@ -8,6 +8,7 @@ #include "services/clock.h" #include "services/network.h" #include "services/screenshot.h" +#include "services/telnet.h" #include "apps/statusbar.h" #include "apps/launcher.h" @@ -20,6 +21,7 @@ void setup() { serviceManager->addService(new NetworkService()); serviceManager->addService(new ClockService()); serviceManager->addService(new ScreenshotService()); + serviceManager->addService(new TelnetService()); appManager->setPanel(new StatusBarApp()); appManager->runApp(new LauncherApp()); } diff --git a/firmware/keira/src/service.cpp b/firmware/keira/src/service.cpp index bb6e4344..5ef0d752 100644 --- a/firmware/keira/src/service.cpp +++ b/firmware/keira/src/service.cpp @@ -7,14 +7,14 @@ Service::~Service() { } void Service::start() { - Serial.println("Starting service " + String(name)); + lilka::serial_log("Starting service %s", name); xTaskCreate(_run, name, stackSize, this, 1, &taskHandle); } void Service::_run(void* arg) { Service* service = static_cast(arg); service->run(); - Serial.println("Service " + String(service->name) + " died"); + lilka::serial_err("Service %s died", service->name); } void Service::setStackSize(uint32_t stackSize) { diff --git a/firmware/keira/src/services/network.cpp b/firmware/keira/src/services/network.cpp index 1bdc09ed..c6910c7c 100644 --- a/firmware/keira/src/services/network.cpp +++ b/firmware/keira/src/services/network.cpp @@ -6,6 +6,11 @@ #include "network.h" +// Macro magic used to convert decimal constant to char[] constant +#define STRX(x) #x +#define STR(x) STRX(x) +#define LILKA_HOSTNAME_PREFIX "LilkaV" + // EEPROM preferences used: // - network.last_ssid - last connected SSID // - network.[SSID_hash]_pw - password of known network with a given SSID diff --git a/firmware/keira/src/services/network.h b/firmware/keira/src/services/network.h index 9af20ae5..38616593 100644 --- a/firmware/keira/src/services/network.h +++ b/firmware/keira/src/services/network.h @@ -2,10 +2,7 @@ #include #include "service.h" -// Macro magic used to convert decimal constant to char[] constant -#define STRX(x) #x -#define STR(x) STRX(x) -#define LILKA_HOSTNAME_PREFIX "LilkaV" + enum NetworkState { NETWORK_STATE_OFFLINE, NETWORK_STATE_CONNECTING, diff --git a/firmware/keira/src/services/telnet.cpp b/firmware/keira/src/services/telnet.cpp new file mode 100644 index 00000000..9a68bc78 --- /dev/null +++ b/firmware/keira/src/services/telnet.cpp @@ -0,0 +1,181 @@ +#include + +#include "telnet.h" +#include "servicemanager.h" +#include "network.h" + +#define STR_HELPER(x) #x +#define STR(x) STR_HELPER(x) + +ESPTelnet telnet; +EscapeCodes ansi; + +TelnetService::TelnetService() : Service("telnet") { +} + +TelnetService::~TelnetService() { +} + +typedef struct { + const char* command; + void (*function)(String); +} Command; + +Command commands[] = { + { + "help", + [](String args) { + telnet.println("Доступні команди:"); + telnet.println(" help - показати цей список команд"); + telnet.println(" reboot - перезавантажити пристрій"); + telnet.println(" uptime - показати час роботи пристрою"); + telnet.println(" free - показати стан пам'яті"); + telnet.println(" ls [DIR] - показати список файлів на SD-картці"); + telnet.println(" find [TEXT] - знайти файли на SD-картці, які містять TEXT в назві"); + telnet.println(" exit - розірвати з'єднання"); + }, + }, + { + "reboot", + [](String args) { + telnet.println("Система перезавантажується..."); + telnet.disconnectClient(); + esp_restart(); + }, + }, + { + "uptime", + [](String args) { telnet.println("Uptime: " + String(millis() / 1000) + " seconds"); }, + }, + { + "free", + [](String args) { + telnet.println("Heap: " + String(ESP.getFreeHeap()) + " / " + String(ESP.getHeapSize()) + " bytes free"); + telnet.println("PSRAM: " + String(ESP.getFreePsram()) + " / " + String(ESP.getPsramSize()) + " bytes free"); + }, + }, + { + "ls", + [](String args) { + File dir = SD.open(args.isEmpty() ? "/" : ("/" + args)); + int count = 0; + if (!dir) { + telnet.println("Не вдалося відкрити директорію: " + args); + return; + } + while (File file = dir.openNextFile()) { + count++; + file.isDirectory() ? telnet.print("DIR ") : telnet.print("FILE"); + telnet.print(" "); + telnet.printf( + "%8s ", !file.isDirectory() ? lilka::fileutils.getHumanFriendlySize(file.size()).c_str() : "-" + ); + telnet.println(file.name()); + file.close(); + } + dir.close(); + telnet.println("Знайдено " + String(count) + " файлів"); + }, + }, + { + "find", + [](String args) { + std::vector dirs; + dirs.push_back("/"); + int count = 0; + while (!dirs.empty()) { + String dir = dirs.back(); + dirs.pop_back(); + File d = SD.open(dir); + if (!d) { + telnet.println("Не вдалося відкрити директорію: " + dir); + continue; + } + while (File file = d.openNextFile()) { + String fullPath = (!dir.equals("/") ? dir : "") + "/" + file.name(); + if (file.isDirectory()) { + dirs.push_back(fullPath); + } else if (args.isEmpty() || String(file.name()).indexOf(args) != -1) { + count++; + telnet.print("FILE "); + telnet.printf("%8s ", lilka::fileutils.getHumanFriendlySize(file.size()).c_str()); + telnet.println(fullPath); + } + file.close(); + } + d.close(); + } + telnet.println("Знайдено " + String(count) + " файлів"); + }, + }, + { + "exit", + [](String args) { telnet.disconnectClient(); }, + }, +}; + +void TelnetService::run() { + NetworkService* network = ServiceManager::getInstance()->getService("network"); + + telnet.onConnectionAttempt([](String ip) { + lilka::serial_log("TelnetService: %s attempting to connect", ip.c_str()); + }); + telnet.onConnect([](String ip) { + lilka::serial_log("TelnetService: %s connected", ip.c_str()); + telnet.print(ansi.cls()); + telnet.print(ansi.home()); + telnet.println(ansi.setBG(ANSI_BLUE) + " " + ansi.reset() + " Keira OS @ Lilka v" STR(LILKA_VERSION)); + telnet.println(ansi.setBG(ANSI_YELLOW) + " " + ansi.reset() + " Слава Україні! "); + telnet.println(); + telnet.print("> "); + }); + telnet.onReconnect([](String ip) { lilka::serial_log("TelnetService: %s reconnected", ip.c_str()); }); + telnet.onDisconnect([](String ip) { lilka::serial_log("TelnetService: %s disconnected", ip.c_str()); }); + telnet.onInputReceived([](String input) { + lilka::serial_log("TelnetService: received text: %s", input.c_str()); + input.trim(); + if (!input.isEmpty()) { + int spaceIndex = input.indexOf(' '); + if (spaceIndex == -1) { + spaceIndex = input.length(); + } + String command = input.substring(0, spaceIndex); + command.toLowerCase(); + command.trim(); + String args = input.substring(spaceIndex + 1); + args.trim(); + Command* cmd = &commands[0]; + for (int i = 0; i < sizeof(commands) / sizeof(Command); i++) { + if (command.equals(commands[i].command)) { + cmd = &commands[i]; + break; + } + } + cmd->function(args); + } + telnet.print("> "); + }); + + bool wasOnline = false; + while (1) { + if (!network) { + vTaskDelay(1000 / portTICK_PERIOD_MS); + continue; + } + bool isOnline = network->getNetworkState() == NetworkState::NETWORK_STATE_ONLINE; + if (!wasOnline && isOnline) { + wasOnline = true; + // Start telnet server + telnet.begin(23); + } else if (wasOnline && !isOnline) { + wasOnline = false; + // Stop telnet server + telnet.stop(); + } + if (isOnline) { + telnet.loop(); + } else { + vTaskDelay(500 / portTICK_PERIOD_MS); + } + } +} diff --git a/firmware/keira/src/services/telnet.h b/firmware/keira/src/services/telnet.h new file mode 100644 index 00000000..5039f689 --- /dev/null +++ b/firmware/keira/src/services/telnet.h @@ -0,0 +1,12 @@ +#include + +#include "service.h" + +class TelnetService : public Service { +public: + TelnetService(); + ~TelnetService(); + +private: + void run() override; +}; diff --git a/sdk/lib/lilka/src/lilka/fileutils.cpp b/sdk/lib/lilka/src/lilka/fileutils.cpp index 06124d19..afcbb3f5 100644 --- a/sdk/lib/lilka/src/lilka/fileutils.cpp +++ b/sdk/lib/lilka/src/lilka/fileutils.cpp @@ -1,6 +1,8 @@ #include "fileutils.h" #include "serial.h" #include "spi.h" +#include "config.h" + namespace lilka { FileUtils::FileUtils() : sdMutex(xSemaphoreCreateMutex()) { sdfs = &SD; @@ -205,12 +207,12 @@ const String FileUtils::getSDRoot() { const String FileUtils::getSPIFFSRoot() { return LILKA_SPIFFS_ROOT; } -const String FileUtils::getHumanFriendlySize(const size_t size) { +const String FileUtils::getHumanFriendlySize(const size_t size, bool compact) { // Max length of file size const char* suffixes[] = {"B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"}; const int numSuffixes = sizeof(suffixes) / sizeof(suffixes[0]); - if (size == 0) return "0B"; + if (size == 0) return String("0") + (compact ? "" : " ") + suffixes[0]; int exp = 0; double dsize = (double)(size); @@ -221,13 +223,13 @@ const String FileUtils::getHumanFriendlySize(const size_t size) { } char buffer[50]; - snprintf(buffer, sizeof(buffer), "%.0f%s", dsize, suffixes[exp]); - String hFileSize(buffer); - while (hFileSize.length() != H_FILE_SIZE) { - hFileSize = hFileSize + " "; + if (compact) { + snprintf(buffer, sizeof(buffer), "%.0f%s", dsize, suffixes[exp]); + } else { + snprintf(buffer, sizeof(buffer), "%.0f %2s", dsize, suffixes[exp]); } - return hFileSize; + return String(buffer); } bool FileUtils::createSDPartTable() { diff --git a/sdk/lib/lilka/src/lilka/fileutils.h b/sdk/lib/lilka/src/lilka/fileutils.h index 67ca148a..2bf91e39 100644 --- a/sdk/lib/lilka/src/lilka/fileutils.h +++ b/sdk/lib/lilka/src/lilka/fileutils.h @@ -1,16 +1,13 @@ #pragma once #include -#include "config.h" #include #include -#include "config.h" #include #include #define LILKA_SD_ROOT "/sd" #define LILKA_SPIFFS_ROOT "/spiffs" #define LILKA_SLASH "/" -#define H_FILE_SIZE 6 #define LILKA_SD_FREQUENCY 20000000 namespace lilka { @@ -137,12 +134,13 @@ class FileUtils { /// Повернути розмір в читабельному форматі (наприклад, 101 MB). /// /// @param size Розмір (в байтах) + /// @param compact Чи виводити розмір компактно (наприклад, 1.23MB замість 1.23 MB) /// @return Рядок, що містить читабельний розмір з суфіксами одиниць виміру /// /// @code /// fileutils.getHumanFriendlySize(1234567); // Поверне "1.23 MB" /// @endcode - const String getHumanFriendlySize(const size_t size); + const String getHumanFriendlySize(const size_t size, bool compact = false); private: SemaphoreHandle_t sdMutex = NULL;