#include "SimulatorProfile.h" #include #include #include #include #include #include #include #include #include #include #ifdef __APPLE__ #include #endif namespace simulator { constexpr auto* TAG = "SimulatorProfile"; constexpr auto* PROFILE_DIRECTORY = "data/tactility"; constexpr auto* PROFILE_PATH = "data/tactility/simulator_profile.txt"; constexpr auto* DISPLAY_SETTINGS_DIRECTORY = "data/tactility/user/app/tactility.display"; constexpr auto* DISPLAY_SETTINGS_PATH = "data/tactility/user/app/tactility.display/display.properties"; constexpr auto* ORIENTATION_KEY = "orientation="; static const Profile PROFILES[PROFILE_COUNT] = { { "Default", 640, 320, false, false, false, true, true }, { "CL-32", 384, 168, false, true, true, true, false }, { "LilyGO T-Deck", 320, 240, false, false, false, true, true }, { "LilyGO T-Lora Pager", 480, 222, false, false, false, true, false }, { "M5Stack Cardputer", 240, 135, false, true, false, true, false }, // Half the res on each axis to compensate for DPI differences { "M5Stack Tab5", 640, 360, true, false, false, true, true }, }; static size_t activeProfileIndex = 0; static OrientationOverride activeOrientation = OrientationOverride::Default; static char** processArguments = nullptr; /** Absolute, because argv[0] can be a bare name that was found through PATH. Empty when it can't be resolved. */ static std::string executablePath; const Profile& getProfile(size_t index) { return PROFILES[index < PROFILE_COUNT ? index : 0]; } static const char* toString(OrientationOverride orientation) { switch (orientation) { case OrientationOverride::Landscape: return "landscape"; case OrientationOverride::Portrait: return "portrait"; default: return "default"; } } /** The first line has the profile name, the optional second line the orientation */ static void loadSelection(size_t& profileIndex, OrientationOverride& orientation) { profileIndex = 0; orientation = OrientationOverride::Default; FILE* file = fopen(PROFILE_PATH, "r"); if (file == nullptr) { return; } char name[64] = {}; char orientationText[16] = {}; const bool read = fgets(name, sizeof(name), file) != nullptr; if (read) { fgets(orientationText, sizeof(orientationText), file); } fclose(file); if (!read) { return; } name[strcspn(name, "\r\n")] = '\0'; orientationText[strcspn(orientationText, "\r\n")] = '\0'; for (size_t i = 0; i < PROFILE_COUNT; i++) { if (strcmp(PROFILES[i].name, name) == 0) { profileIndex = i; } } for (auto candidate : { OrientationOverride::Landscape, OrientationOverride::Portrait }) { if (strcmp(toString(candidate), orientationText) == 0) { orientation = candidate; } } } size_t loadSelectedProfileIndex() { size_t profileIndex; OrientationOverride orientation; loadSelection(profileIndex, orientation); return profileIndex; } OrientationOverride loadSelectedOrientation() { size_t profileIndex; OrientationOverride orientation; loadSelection(profileIndex, orientation); return orientation; } static const char* toDisplaySettingsValue(OrientationOverride orientation) { return orientation == OrientationOverride::Portrait ? "Portrait" : "Landscape"; } /** Creates the folders of a path, like "mkdir -p" */ static void makeDirectories(const std::string& path) { for (size_t position = path.find('/'); position != std::string::npos; position = path.find('/', position + 1)) { mkdir(path.substr(0, position).c_str(), 0755); } mkdir(path.c_str(), 0755); } /** * The Display settings apply their saved orientation to the display. Writes the override there, * or removes the saved orientation so that the display's own orientation applies. */ static void applyOrientationToDisplaySettings(OrientationOverride orientation) { std::string kept; if (FILE* file = fopen(DISPLAY_SETTINGS_PATH, "r"); file != nullptr) { char line[256]; while (fgets(line, sizeof(line), file) != nullptr) { if (strncmp(line, ORIENTATION_KEY, strlen(ORIENTATION_KEY)) != 0) { kept += line; } } fclose(file); } if (orientation != OrientationOverride::Default) { kept += std::string(ORIENTATION_KEY) + toDisplaySettingsValue(orientation) + "\n"; } else if (kept.empty() && access(DISPLAY_SETTINGS_PATH, F_OK) != 0) { return; } makeDirectories(DISPLAY_SETTINGS_DIRECTORY); FILE* file = fopen(DISPLAY_SETTINGS_PATH, "w"); if (file == nullptr) { LOG_E(TAG, "Failed to write %s", DISPLAY_SETTINGS_PATH); return; } fputs(kept.c_str(), file); fclose(file); } bool saveSelection(size_t profileIndex, OrientationOverride orientation) { size_t savedProfileIndex; OrientationOverride savedOrientation; loadSelection(savedProfileIndex, savedOrientation); const bool changed = profileIndex != savedProfileIndex || orientation != savedOrientation; mkdir(PROFILE_DIRECTORY, 0755); FILE* file = fopen(PROFILE_PATH, "w"); if (file == nullptr) { LOG_E(TAG, "Failed to write %s", PROFILE_PATH); return false; } const bool written = fprintf(file, "%s\n%s\n", getProfile(profileIndex).name, toString(orientation)) > 0; if (fclose(file) != 0 || !written) { return false; } if (changed) { applyOrientationToDisplaySettings(orientation); } return true; } void initActiveProfile() { loadSelection(activeProfileIndex, activeOrientation); LOG_I(TAG, "Profile: %s, orientation: %s", getProfile(activeProfileIndex).name, toString(activeOrientation)); } size_t getActiveProfileIndex() { return activeProfileIndex; } OrientationOverride getActiveOrientation() { return activeOrientation; } static std::string resolveExecutablePath(const char* argv0) { char path[PATH_MAX]; #ifdef __APPLE__ uint32_t size = sizeof(path); char resolved[PATH_MAX]; if (_NSGetExecutablePath(path, &size) == 0 && realpath(path, resolved) != nullptr) { return resolved; } #else const ssize_t length = readlink("/proc/self/exe", path, sizeof(path) - 1); if (length > 0) { path[length] = '\0'; return path; } #endif // Only a path with a slash can be resolved without searching PATH if (strchr(argv0, '/') != nullptr && realpath(argv0, path) != nullptr) { return path; } return ""; } void setProcessArguments(char** argv) { processArguments = argv; executablePath = resolveExecutablePath(argv[0]); } void reboot() { if (processArguments == nullptr) { LOG_E(TAG, "Cannot reboot without process arguments"); return; } // The FreeRTOS tick timer would outlive exec() and kill the new image with SIGALRM itimerval previousTimer = {}; itimerval stopTimer = {}; setitimer(ITIMER_REAL, &stopTimer, &previousTimer); if (!executablePath.empty()) { execv(executablePath.c_str(), processArguments); } else { execvp(processArguments[0], processArguments); } LOG_E(TAG, "Failed to reboot: %s", strerror(errno)); // This process keeps running, so it needs its ticks again setitimer(ITIMER_REAL, &previousTimer, nullptr); } }