// ESP32 DevKit + ILI9341 TFT
// BIOS-style POST + Setup + Boot Menu + Error Screen
// + On-screen Tiny_OS UI with apps + Serial terminal + simple VFS
// Pins: CS=5, DC=2, RST=4, MOSI=23, MISO=19, SCK=18
// Control from Serial Monitor
// CODE_FILE -> enter BIOS flow (POST → Boot Menu → Tiny_OS UI)
// EXIT_BIOS -> return to terminal (from OS RUNNING)
// EXIT -> exit apps / OS UI (see behavior in runOS / terminal)
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
#include <Preferences.h>
#include <esp_system.h>
#define TFT_CS 5
#define TFT_DC 2
#define TFT_RST 4
#define TFT_MOSI 23
#define TFT_MISO 19
#define TFT_SCLK 18
Adafruit_ILI9341 tft(TFT_CS, TFT_DC, TFT_RST);
// ===== MODES =====
enum UIMode {
MODE_TERMINAL,
MODE_BIOS
};
UIMode uiMode = MODE_TERMINAL;
// ===== BIOS STATE =====
enum BiosState {
STATE_POST,
STATE_SETUP,
STATE_BOOT_MENU,
STATE_ERROR,
STATE_OS_RUNNING
};
enum BootProfile {
BOOT_PROFILE_NORMAL = 0,
BOOT_PROFILE_SAFE = 1,
BOOT_PROFILE_DEBUG = 2,
BOOT_PROFILE_RECOVERY = 3
};
enum ErrorType {
ERR_NONE = 0,
ERR_CMOS_CHECKSUM,
ERR_STORAGE,
ERR_NETWORK,
ERR_WATCHDOG,
ERR_CRITICAL
};
struct CmosSettings {
uint8_t bootProfile;
uint8_t bootTimeoutSec;
bool verboseBoot;
bool safeMode;
bool debugMode;
bool recoveryMode;
uint8_t bootOrder[3]; // 0=Flash,1=NVS,2=Network (cosmetic)
bool watchdogEnabled;
uint32_t checksum;
};
BiosState biosState = STATE_POST;
ErrorType currentError = ERR_NONE;
CmosSettings cmos;
Preferences prefs;
unsigned long postStart = 0;
bool cmosLoaded = false;
bool cmosChecksumOK = false;
bool setupDirty = false;
int setupTab = 0;
int setupSelection = 0;
int bootSelection = 0;
unsigned long bootMenuStart = 0;
bool inBootMenu = false;
bool osRunning = false;
unsigned long lastBlink = 0;
bool cursorOn = true;
unsigned long lastWatchdogFeed = 0;
// ===== TERMINAL CONFIG =====
#define TERM_MAX_LINES 64
#define TERM_LINE_HEIGHT 10
#define TERM_COLS 40
String termLines[TERM_MAX_LINES];
int termLineCount = 0;
String termInput = "";
bool termNeedsRedraw = true;
// ===== SIMPLE VFS =====
struct VfsFile {
String name;
String content;
};
#define VFS_MAX_FILES 16
VfsFile vfs[VFS_MAX_FILES];
int vfsCount = 0;
// ===== OS APP MENU =====
enum OsApp {
OS_APP_CLOCK = 0,
OS_APP_CALENDAR,
OS_APP_NOTES,
OS_APP_TERMINAL_INFO,
OS_APP_CALC,
OS_APP_FILES,
OS_APP_COUNT
};
int osSelectedApp = 0;
bool osInApp = false;
// Calculator state
String calcLastExpr = "";
String calcLastResult = "";
// ===== UTILS =====
uint32_t calcChecksum(const CmosSettings &s) {
const uint8_t *p = (const uint8_t*)&s;
size_t len = sizeof(CmosSettings) - sizeof(uint32_t);
uint32_t crc = 0xFFFFFFFF;
for (size_t i = 0; i < len; i++) {
crc ^= p[i];
for (int j = 0; j < 8; j++) {
if (crc & 1) crc = (crc >> 1) ^ 0xEDB88320;
else crc >>= 1;
}
}
return crc;
}
void clearScreen(uint16_t color = ILI9341_BLACK) {
tft.fillScreen(color);
tft.setCursor(0, 0);
}
void drawBlinkCursor(int x, int y) {
if (millis() - lastBlink > 500) {
lastBlink = millis();
cursorOn = !cursorOn;
}
if (cursorOn) {
tft.fillRect(x, y, 8, 2, ILI9341_WHITE);
}
}
void drawProgressBar(int x, int y, int w, int h, float progress) {
tft.drawRect(x, y, w, h, ILI9341_WHITE);
int fill = (int)((w - 2) * constrain(progress, 0.0f, 1.0f));
tft.fillRect(x + 1, y + 1, fill, h - 2, ILI9341_WHITE);
}
void drawSpinner(int x, int y, int frame) {
const char frames[4] = {'|', '/', '-', '\\'};
tft.setCursor(x, y);
tft.setTextSize(2);
tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);
tft.print(frames[frame % 4]);
}
void feedWatchdog() {
lastWatchdogFeed = millis();
}
void checkSoftwareWatchdog() {
if (cmos.watchdogEnabled) {
if (millis() - lastWatchdogFeed > 8000) {
currentError = ERR_WATCHDOG;
biosState = STATE_ERROR;
uiMode = MODE_BIOS;
}
}
}
// ===== SERIAL INPUT (BIOS KEYS, WASD+F) =====
void readBiosInput(int &dx, int &dy, bool &enter, bool &esc, bool &f1, bool &f2, bool &f9, bool &f10, bool &del) {
dx = dy = 0;
enter = esc = f1 = f2 = f9 = f10 = del = false;
static int escState = 0;
while (Serial.available()) {
int c = Serial.peek();
// Printable chars go to terminal, not BIOS navigation
if ((c >= 32 && c <= 126) || c == '\r' || c == '\n') {
return;
}
c = Serial.read();
if (escState == 0 && c == 27) { escState = 1; continue; }
if (escState == 1 && c == '[') { escState = 2; continue; }
if (escState == 2) {
if (c == 'A') dy = -1;
else if (c == 'B') dy = 1;
else if (c == 'C') dx = 1;
else if (c == 'D') dx = -1;
escState = 0;
continue;
}
escState = 0;
if (c == '\r' || c == '\n') enter = true;
else if (c == 27) esc = true;
else if (c == '1') f1 = true;
else if (c == '2') f2 = true;
else if (c == '9') f9 = true;
else if (c == '0') f10 = true;
else if (c == 'd' || c == 'D') del = true;
else if (c == 'f' || c == 'F') enter = true; // F = select
if (c == 'w' || c == 'W') dy = -1;
if (c == 's' || c == 'S') dy = 1;
if (c == 'a' || c == 'A') dx = -1;
if (c == 'd' || c == 'D') dx = 1;
}
}
// ===== CMOS =====
void loadDefaultCmos() {
cmos.bootProfile = BOOT_PROFILE_NORMAL;
cmos.bootTimeoutSec = 5;
cmos.verboseBoot = true;
cmos.safeMode = false;
cmos.debugMode = false;
cmos.recoveryMode = false;
cmos.bootOrder[0] = 0;
cmos.bootOrder[1] = 1;
cmos.bootOrder[2] = 2;
cmos.watchdogEnabled = true;
cmos.checksum = calcChecksum(cmos);
}
void saveCmos() {
cmos.checksum = calcChecksum(cmos);
prefs.begin("bios", false);
prefs.putBytes("cmos", &cmos, sizeof(CmosSettings));
prefs.end();
}
void loadCmos() {
prefs.begin("bios", true);
if (prefs.isKey("cmos")) {
prefs.getBytes("cmos", &cmos, sizeof(CmosSettings));
uint32_t crc = calcChecksum(cmos);
cmosChecksumOK = (crc == cmos.checksum);
} else {
loadDefaultCmos();
cmosChecksumOK = true;
}
prefs.end();
cmosLoaded = true;
}
// ===== REAL TESTS =====
bool testRAM(String &detail) {
size_t before = ESP.getFreeHeap();
const size_t allocSize = 16 * 1024;
uint8_t *buf = (uint8_t*)malloc(allocSize);
if (!buf) {
detail = "malloc failed";
return false;
}
for (size_t i = 0; i < allocSize; i++) buf[i] = (uint8_t)(i & 0xFF);
size_t sum = 0;
for (size_t i = 0; i < allocSize; i++) sum += buf[i];
free(buf);
size_t after = ESP.getFreeHeap();
detail = "heap " + String(before) + " -> " + String(after) + " sum=" + String(sum);
return true;
}
bool testCPU(String &detail) {
unsigned long start = micros();
volatile uint32_t acc = 0;
for (uint32_t i = 0; i < 500000; i++) {
acc += i ^ (i << 1);
}
unsigned long elapsed = micros() - start;
detail = "loop 500k in " + String(elapsed / 1000.0, 2) + " ms";
return true;
}
bool testStorage(String &detail) {
Preferences p;
if (!p.begin("stor", false)) {
detail = "NVS open failed";
return false;
}
const char *key = "test";
const char *val = "STORAGE_TEST_OK";
p.putString(key, val);
String readBack = p.getString(key, "");
p.end();
detail = "NVS read: " + readBack;
return readBack == val;
}
bool testNetwork(String &detail) {
uint64_t mac = ESP.getEfuseMac();
char buf[18];
sprintf(buf, "%02X:%02X:%02X:%02X:%02X:%02X",
(uint8_t)(mac >> 40),
(uint8_t)(mac >> 32),
(uint8_t)(mac >> 24),
(uint8_t)(mac >> 16),
(uint8_t)(mac >> 8),
(uint8_t)(mac));
detail = "eFuse MAC " + String(buf);
return true;
}
// ===== POST =====
void drawPostHeader() {
clearScreen(ILI9341_BLACK);
tft.setTextSize(2);
tft.setTextColor(ILI9341_GREEN, ILI9341_BLACK);
tft.setCursor(0, 0);
tft.println("Computer_IBM_TEST");
tft.setTextSize(1);
tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);
tft.println("Tiny_OS BIOS POST");
}
void drawPostInfo() {
tft.println();
tft.print("CPU: ");
tft.println(ESP.getChipModel());
tft.print("Freq: ");
tft.print(ESP.getCpuFreqMHz());
tft.println(" MHz");
tft.print("Flash: ");
tft.print(ESP.getFlashChipSize() / (1024 * 1024));
tft.println(" MB");
tft.print("Heap: ");
tft.print(ESP.getFreeHeap());
tft.println(" bytes");
tft.println();
}
void runPost() {
if (!cmosLoaded) loadCmos();
drawPostHeader();
drawPostInfo();
tft.println("Running hardware tests...");
String detail;
tft.print("RAM test: ");
if (testRAM(detail)) tft.println("OK");
else { tft.println("FAIL"); currentError = ERR_CRITICAL; }
tft.println(" " + detail);
tft.print("CPU test: ");
if (testCPU(detail)) tft.println("OK");
else { tft.println("FAIL"); currentError = ERR_CRITICAL; }
tft.println(" " + detail);
tft.print("Storage test: ");
if (testStorage(detail)) tft.println("OK");
else { tft.println("FAIL"); currentError = ERR_STORAGE; }
tft.println(" " + detail);
tft.print("Network test: ");
if (testNetwork(detail)) tft.println("OK");
else { tft.println("FAIL"); currentError = ERR_NETWORK; }
tft.println(" " + detail);
tft.println();
tft.print("CMOS checksum: ");
if (cmosChecksumOK) tft.println("OK");
else {
tft.println("INVALID");
currentError = ERR_CMOS_CHECKSUM;
}
tft.println();
tft.println("Press DEL for Setup");
tft.println("Auto boot in 3s...");
postStart = millis();
while (millis() - postStart < 3000 && uiMode == MODE_BIOS) {
int dx, dy; bool enter, esc, f1, f2, f9, f10, del;
readBiosInput(dx, dy, enter, esc, f1, f2, f9, f10, del);
if (del) {
biosState = STATE_SETUP;
return;
}
feedWatchdog();
checkSoftwareWatchdog();
delay(20);
}
if (currentError != ERR_NONE) {
biosState = STATE_ERROR;
} else {
biosState = STATE_BOOT_MENU;
}
}
// ===== SETUP (BLUE UI) =====
const char* setupTabs[5] = {"Main", "Advanced", "Boot", "Security", "Exit"};
void drawSetupHeader() {
clearScreen(ILI9341_BLUE);
tft.setTextSize(2);
tft.setTextColor(ILI9341_WHITE, ILI9341_BLUE);
tft.setCursor(0, 0);
for (int i = 0; i < 5; i++) {
if (i == setupTab) {
tft.setTextColor(ILI9341_YELLOW, ILI9341_BLUE);
} else {
tft.setTextColor(ILI9341_WHITE, ILI9341_BLUE);
}
tft.print(" ");
tft.print(setupTabs[i]);
}
tft.setTextColor(ILI9341_WHITE, ILI9341_BLUE);
tft.setCursor(0, 20);
tft.setTextSize(1);
}
void drawMainTab() {
tft.println("System Summary");
tft.println("----------------");
tft.print("CPU: ");
tft.println(ESP.getChipModel());
tft.print("Freq: ");
tft.print(ESP.getCpuFreqMHz());
tft.println(" MHz");
tft.print("Flash: ");
tft.print(ESP.getFlashChipSize() / (1024 * 1024));
tft.println(" MB");
tft.print("Heap: ");
tft.print(ESP.getFreeHeap());
tft.println(" bytes");
tft.println();
tft.print("Boot profile: ");
switch (cmos.bootProfile) {
case BOOT_PROFILE_NORMAL: tft.println("Normal"); break;
case BOOT_PROFILE_SAFE: tft.println("Safe"); break;
case BOOT_PROFILE_DEBUG: tft.println("Debug"); break;
case BOOT_PROFILE_RECOVERY: tft.println("Recovery"); break;
}
}
void drawAdvancedTab() {
tft.println("Advanced Settings");
tft.println("-----------------");
tft.print("Verbose boot: ");
tft.println(cmos.verboseBoot ? "ON" : "OFF");
tft.print("Safe mode: ");
tft.println(cmos.safeMode ? "ON" : "OFF");
tft.print("Debug mode: ");
tft.println(cmos.debugMode ? "ON" : "OFF");
tft.print("Recovery mode: ");
tft.println(cmos.recoveryMode ? "ON" : "OFF");
tft.println();
tft.print("Watchdog: ");
tft.println(cmos.watchdogEnabled ? "Enabled" : "Disabled");
}
void drawBootTab() {
tft.println("Boot Settings");
tft.println("-------------");
tft.print("Timeout: ");
tft.print(cmos.bootTimeoutSec);
tft.println(" s");
tft.println("Boot order:");
const char* devs[3] = {"Flash", "NVS", "Network"};
for (int i = 0; i < 3; i++) {
tft.print(" ");
tft.print(i + 1);
tft.print(": ");
tft.println(devs[cmos.bootOrder[i]]);
}
}
void drawSecurityTab() {
tft.println("Security");
tft.println("--------");
tft.println("Supervisor password: Not set");
tft.println("Secure boot: Enabled");
tft.println("TPM: Emulated");
}
void drawExitTab() {
tft.println("Exit");
tft.println("----");
tft.println("1) Save changes and reset");
tft.println("2) Discard changes and exit");
tft.println("3) Load setup defaults");
tft.println();
tft.println("Use WASD + F");
}
void drawSetupMenu() {
drawSetupHeader();
switch (setupTab) {
case 0: drawMainTab(); break;
case 1: drawAdvancedTab(); break;
case 2: drawBootTab(); break;
case 3: drawSecurityTab(); break;
case 4: drawExitTab(); break;
}
int y = 32 + setupSelection * 10;
tft.drawRect(0, y, tft.width(), 10, ILI9341_YELLOW);
drawBlinkCursor(tft.width() - 10, tft.height() - 10);
}
void handleSetupInput() {
int dx, dy; bool enter, esc, f1, f2, f9, f10, del;
readBiosInput(dx, dy, enter, esc, f1, f2, f9, f10, del);
if (dx != 0) {
setupTab = (setupTab + dx + 5) % 5;
setupSelection = 0;
}
if (dy != 0) {
setupSelection = constrain(setupSelection + dy, 0, 5);
}
if (f9) {
loadDefaultCmos();
setupDirty = true;
}
if (f10) {
saveCmos();
setupDirty = false;
biosState = STATE_BOOT_MENU;
}
if (esc) {
if (setupDirty) {
clearScreen(ILI9341_BLUE);
tft.setTextColor(ILI9341_WHITE, ILI9341_BLUE);
tft.setCursor(0, 40);
tft.println("Discard changes?");
tft.println("F1 = Yes, ESC = No");
unsigned long t0 = millis();
while (millis() - t0 < 5000 && uiMode == MODE_BIOS) {
int dx2, dy2; bool e2, esc2, f12, f22, f92, f102, del2;
readBiosInput(dx2, dy2, e2, esc2, f12, f22, f92, f102, del2);
if (f12) {
setupDirty = false;
biosState = STATE_BOOT_MENU;
return;
}
if (esc2) break;
delay(20);
}
} else {
biosState = STATE_BOOT_MENU;
}
}
if (enter) {
setupDirty = true;
if (setupTab == 1) {
if (setupSelection == 0) cmos.verboseBoot = !cmos.verboseBoot;
if (setupSelection == 1) cmos.safeMode = !cmos.safeMode;
if (setupSelection == 2) cmos.debugMode = !cmos.debugMode;
if (setupSelection == 3) cmos.recoveryMode = !cmos.recoveryMode;
if (setupSelection == 4) cmos.watchdogEnabled = !cmos.watchdogEnabled;
} else if (setupTab == 2) {
if (setupSelection == 0) {
cmos.bootTimeoutSec = (cmos.bootTimeoutSec % 10) + 1;
} else if (setupSelection >= 1 && setupSelection <= 3) {
int idx = setupSelection - 1;
cmos.bootOrder[idx] = (cmos.bootOrder[idx] + 1) % 3;
}
} else if (setupTab == 4) {
if (setupSelection == 0) {
saveCmos();
setupDirty = false;
biosState = STATE_BOOT_MENU;
} else if (setupSelection == 1) {
setupDirty = false;
biosState = STATE_BOOT_MENU;
} else if (setupSelection == 2) {
loadDefaultCmos();
setupDirty = true;
}
}
}
}
void runSetup() {
while (biosState == STATE_SETUP && uiMode == MODE_BIOS) {
drawSetupMenu();
handleSetupInput();
feedWatchdog();
checkSoftwareWatchdog();
delay(60);
}
}
// ===== BOOT MENU =====
const char* bootProfiles[4] = {
"Normal Mode",
"Safe Mode",
"Debug Mode",
"Recovery Mode"
};
void drawBootMenu(float progress, int spinnerFrame, int countdown) {
clearScreen(ILI9341_BLACK);
tft.setTextSize(2);
tft.setTextColor(ILI9341_CYAN, ILI9341_BLACK);
tft.setCursor(0, 0);
tft.println("Boot Manager");
tft.setTextSize(1);
tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);
tft.println();
tft.println("Select boot profile (WASD + F):");
for (int i = 0; i < 4; i++) {
if (i == bootSelection) {
tft.setTextColor(ILI9341_YELLOW, ILI9341_BLACK);
tft.print("> ");
} else {
tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);
tft.print(" ");
}
tft.println(bootProfiles[i]);
}
tft.println();
tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);
tft.print("Timeout: ");
tft.print(countdown);
tft.println(" s");
drawProgressBar(10, 180, 220, 10, progress);
drawSpinner(270, 180, spinnerFrame);
tft.setCursor(10, 200);
tft.print("Loading Tiny_OS...");
}
// small Win95-style ASCII boot screen
void drawTinyOSBootScreen() {
clearScreen(ILI9341_BLACK);
tft.setTextSize(2);
tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);
tft.setCursor(20, 40);
tft.println("Tiny_OS");
tft.setTextSize(1);
tft.setCursor(20, 70);
tft.println("--------------------------------");
tft.setCursor(20, 90);
tft.println("Booting Tiny_OS...");
tft.setCursor(20, 110);
tft.println("Please wait");
tft.setCursor(20, 140);
tft.println("[##########################]");
delay(1200);
}
void runBootMenu() {
bootSelection = cmos.bootProfile;
bootMenuStart = millis();
inBootMenu = true;
int countdown = cmos.bootTimeoutSec;
int spinnerFrame = 0;
unsigned long lastSecond = millis();
while (inBootMenu && uiMode == MODE_BIOS) {
float elapsed = (millis() - bootMenuStart) / 1000.0f;
float progress = elapsed / cmos.bootTimeoutSec;
if (progress > 1.0f) progress = 1.0f;
drawBootMenu(progress, spinnerFrame, countdown);
spinnerFrame++;
int dx, dy; bool enter, esc, f1, f2, f9, f10, del;
readBiosInput(dx, dy, enter, esc, f1, f2, f9, f10, del);
if (dy != 0) {
bootSelection = (bootSelection + dy + 4) % 4;
}
if (enter) {
inBootMenu = false;
break;
}
if (esc) {
biosState = STATE_SETUP;
return;
}
if (millis() - lastSecond >= 1000) {
lastSecond = millis();
countdown--;
if (countdown <= 0) {
inBootMenu = false;
break;
}
}
feedWatchdog();
checkSoftwareWatchdog();
delay(80);
}
cmos.bootProfile = bootSelection;
saveCmos();
cmos.safeMode = (bootSelection == BOOT_PROFILE_SAFE);
cmos.debugMode = (bootSelection == BOOT_PROFILE_DEBUG);
cmos.recoveryMode = (bootSelection == BOOT_PROFILE_RECOVERY);
cmos.verboseBoot = !cmos.safeMode;
drawTinyOSBootScreen();
biosState = STATE_OS_RUNNING;
}
// ===== ERROR / BSOD-LIKE =====
void drawErrorScreen() {
clearScreen(ILI9341_BLUE);
tft.setTextColor(ILI9341_WHITE, ILI9341_BLUE);
tft.setTextSize(2);
tft.setCursor(0, 0);
tft.println("SYSTEM ERROR");
tft.setTextSize(1);
tft.println();
switch (currentError) {
case ERR_CMOS_CHECKSUM:
tft.println("CMOS checksum invalid.");
tft.println("Defaults will be loaded.");
break;
case ERR_STORAGE:
tft.println("Storage test failed.");
tft.println("NVS may be corrupted.");
break;
case ERR_NETWORK:
tft.println("Network test failed.");
tft.println("Check MAC / config.");
break;
case ERR_WATCHDOG:
tft.println("Software watchdog timeout.");
tft.println("System became unresponsive.");
break;
case ERR_CRITICAL:
tft.println("Critical hardware test failure.");
break;
default:
tft.println("Unknown error.");
break;
}
tft.println();
tft.println("Press F1 to continue");
tft.println("Press F2 for Setup");
}
void runError() {
drawErrorScreen();
while (biosState == STATE_ERROR && uiMode == MODE_BIOS) {
int dx, dy; bool enter, esc, f1, f2, f9, f10, del;
readBiosInput(dx, dy, enter, esc, f1, f2, f9, f10, del);
if (f1) {
if (!cmosChecksumOK) {
loadDefaultCmos();
saveCmos();
}
currentError = ERR_NONE;
biosState = STATE_BOOT_MENU;
}
if (f2) {
biosState = STATE_SETUP;
}
delay(50);
}
}
// ===== VFS HELPERS =====
int vfsFindFile(const String &name) {
for (int i = 0; i < vfsCount; i++) {
if (vfs[i].name.equalsIgnoreCase(name)) return i;
}
return -1;
}
bool vfsCreateOrOverwrite(const String &name, const String &content) {
int idx = vfsFindFile(name);
if (idx >= 0) {
vfs[idx].content = content;
return true;
}
if (vfsCount >= VFS_MAX_FILES) return false;
vfs[vfsCount].name = name;
vfs[vfsCount].content = content;
vfsCount++;
return true;
}
bool vfsAppend(const String &name, const String &content) {
int idx = vfsFindFile(name);
if (idx >= 0) {
vfs[idx].content += content;
return true;
}
if (vfsCount >= VFS_MAX_FILES) return false;
vfs[vfsCount].name = name;
vfs[vfsCount].content = content;
vfsCount++;
return true;
}
bool vfsDelete(const String &name) {
int idx = vfsFindFile(name);
if (idx < 0) return false;
for (int i = idx; i < vfsCount - 1; i++) {
vfs[i] = vfs[i + 1];
}
vfsCount--;
return true;
}
// ===== OS APP DRAWING =====
void iconClock(int x, int y, uint16_t color) {
tft.drawCircle(x+12, y+12, 10, color);
tft.drawLine(x+12, y+12, x+12, y+6, color);
tft.drawLine(x+12, y+12, x+18, y+12, color);
}
void iconCalendar(int x, int y, uint16_t color) {
tft.drawRect(x+4, y+4, 16, 16, color);
tft.drawLine(x+4, y+8, x+20, y+8, color);
}
void iconNotes(int x, int y, uint16_t color) {
tft.drawRect(x+4, y+4, 16, 16, color);
tft.drawLine(x+6, y+8, x+18, y+8, color);
tft.drawLine(x+6, y+12, x+18, y+12, color);
}
void iconTerminal(int x, int y, uint16_t color) {
tft.drawRect(x+4, y+4, 16, 16, color);
tft.setCursor(x+6, y+8);
tft.setTextSize(1);
tft.setTextColor(color, ILI9341_BLACK);
tft.print(">");
}
void iconCalc(int x, int y, uint16_t color) {
tft.drawRect(x+4, y+4, 16, 16, color);
tft.drawLine(x+10, y+6, x+14, y+6, color);
tft.drawLine(x+12, y+4, x+12, y+8, color);
tft.drawLine(x+10, y+14, x+14, y+14, color);
}
void iconFiles(int x, int y, uint16_t color) {
tft.drawRect(x+6, y+4, 14, 16, color);
tft.drawLine(x+6, y+8, x+20, y+8, color);
}
// 3x2 grid
void drawOsMenu() {
clearScreen(ILI9341_BLACK);
tft.setTextSize(2);
tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);
tft.setCursor(10, 5);
tft.println("Tiny_OS");
tft.setTextSize(1);
tft.setCursor(10, 28);
tft.setTextColor(ILI9341_CYAN, ILI9341_BLACK);
tft.println("WASD to move, F to select");
tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);
const char* names[OS_APP_COUNT] = {
"Clock",
"Calendar",
"Notes",
"Terminal",
"Calc",
"Files"
};
int cols = 3;
int rows = 2;
int idx = 0;
int startY = 60;
int cellW = 100;
int cellH = 70;
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
if (idx >= OS_APP_COUNT) break;
int x = 10 + c * cellW;
int y = startY + r * cellH;
uint16_t border = (idx == osSelectedApp) ? ILI9341_YELLOW : ILI9341_DARKGREY;
tft.drawRoundRect(x, y, cellW - 20, cellH - 20, 8, border);
uint16_t iconColor = ILI9341_CYAN;
int iconX = x + 8;
int iconY = y + 8;
switch (idx) {
case OS_APP_CLOCK: iconClock(iconX, iconY, iconColor); break;
case OS_APP_CALENDAR: iconCalendar(iconX, iconY, iconColor); break;
case OS_APP_NOTES: iconNotes(iconX, iconY, iconColor); break;
case OS_APP_TERMINAL_INFO: iconTerminal(iconX, iconY, iconColor); break;
case OS_APP_CALC: iconCalc(iconX, iconY, iconColor); break;
case OS_APP_FILES: iconFiles(iconX, iconY, iconColor); break;
}
tft.setCursor(x + 40, y + 18);
tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);
tft.print(names[idx]);
idx++;
}
}
}
// Clock app
void drawClockApp() {
clearScreen(ILI9341_BLACK);
tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);
tft.setTextSize(2);
tft.setCursor(10, 5);
tft.println("Clock");
int cx = tft.width() / 2;
int cy = tft.height() / 2 + 10;
int radius = 80;
tft.drawCircle(cx, cy, radius, ILI9341_WHITE);
for (int i = 0; i < 12; i++) {
float angle = (i / 12.0f) * 2 * PI;
int x1 = cx + cos(angle) * (radius - 5);
int y1 = cy + sin(angle) * (radius - 5);
int x2 = cx + cos(angle) * (radius - 15);
int y2 = cy + sin(angle) * (radius - 15);
tft.drawLine(x1, y1, x2, y2, ILI9341_WHITE);
}
unsigned long ms = millis();
unsigned long sec = (ms / 1000) % 60;
unsigned long min = (ms / (1000 * 60)) % 60;
unsigned long hr = (ms / (1000 * 60 * 60)) % 12;
float secAngle = (sec / 60.0f) * 2 * PI - PI / 2;
float minAngle = ((min + sec / 60.0f) / 60.0f) * 2 * PI - PI / 2;
float hrAngle = ((hr + min / 60.0f) / 12.0f) * 2 * PI - PI / 2;
int sx = cx + cos(secAngle) * (radius - 10);
int sy = cy + sin(secAngle) * (radius - 10);
int mx = cx + cos(minAngle) * (radius - 20);
int my = cy + sin(minAngle) * (radius - 20);
int hx = cx + cos(hrAngle) * (radius - 35);
int hy = cy + sin(hrAngle) * (radius - 35);
tft.drawLine(cx, cy, hx, hy, ILI9341_WHITE);
tft.drawLine(cx, cy, mx, my, ILI9341_CYAN);
tft.drawLine(cx, cy, sx, sy, ILI9341_RED);
tft.setTextSize(1);
tft.setCursor(10, 220);
tft.setTextColor(ILI9341_YELLOW, ILI9341_BLACK);
tft.println("Type EXIT in Serial to return.");
}
void drawCalendarApp() {
clearScreen(ILI9341_BLACK);
tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);
tft.setTextSize(2);
tft.setCursor(10, 5);
tft.println("Calendar");
tft.setTextSize(1);
tft.setCursor(10, 30);
tft.println("Static month view (no RTC).");
const char* days[7] = {"Su","Mo","Tu","We","Th","Fr","Sa"};
int startX = 10;
int startY = 50;
int cellW = 30;
int cellH = 20;
for (int i = 0; i < 7; i++) {
tft.setCursor(startX + i * cellW, startY);
tft.print(days[i]);
}
int day = 1;
int row = 1;
int col = 1; // start Monday
for (; day <= 30; day++) {
int x = startX + col * cellW;
int y = startY + row * cellH;
tft.setCursor(x, y);
tft.print(day);
col++;
if (col >= 7) {
col = 0;
row++;
}
}
tft.setCursor(10, 220);
tft.setTextColor(ILI9341_YELLOW, ILI9341_BLACK);
tft.println("Type EXIT in Serial to return.");
}
void drawNotesApp() {
clearScreen(ILI9341_BLACK);
tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);
tft.setTextSize(2);
tft.setCursor(10, 5);
tft.println("Notes");
tft.setTextSize(1);
tft.setCursor(10, 30);
tft.println("Files ending with .note are shown.");
int y = 50;
for (int i = 0; i < vfsCount && y < tft.height() - 20; i++) {
if (vfs[i].name.endsWith(".note")) {
tft.setCursor(10, y);
tft.print(vfs[i].name);
y += 12;
}
}
tft.setCursor(10, 220);
tft.setTextColor(ILI9341_YELLOW, ILI9341_BLACK);
tft.println("Use WRITE/APPEND in terminal.");
tft.setCursor(10, 232);
tft.println("Type EXIT in Serial to return.");
}
void drawTerminalInfoApp() {
clearScreen(ILI9341_BLACK);
tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);
tft.setTextSize(2);
tft.setCursor(10, 5);
tft.println("Terminal");
tft.setTextSize(1);
tft.setCursor(10, 30);
tft.println("Use Serial Monitor as shell.");
tft.println();
tft.println("Commands:");
tft.println(" HELP, LS, CAT, WRITE");
tft.println(" APPEND, RM, INFO");
tft.println(" CODE_FILE (enter BIOS)");
tft.println(" EXIT_BIOS (from BIOS OS)");
tft.println(" CALC a b op");
tft.println(" EXIT (leave apps/OS)");
tft.println();
tft.setTextColor(ILI9341_YELLOW, ILI9341_BLACK);
tft.println("Type EXIT in Serial to return.");
}
void drawCalcApp() {
clearScreen(ILI9341_BLACK);
tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);
tft.setTextSize(2);
tft.setCursor(10, 5);
tft.println("Calc");
tft.setTextSize(1);
tft.setCursor(10, 30);
tft.println("Use terminal command:");
tft.println(" CALC <a> <b> <op>");
tft.println("op: + - * /");
tft.println();
tft.println("Last expr:");
tft.setTextColor(ILI9341_CYAN, ILI9341_BLACK);
tft.println(" " + calcLastExpr);
tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);
tft.println("Result:");
tft.setTextColor(ILI9341_GREEN, ILI9341_BLACK);
tft.println(" " + calcLastResult);
tft.setTextColor(ILI9341_YELLOW, ILI9341_BLACK);
tft.setCursor(10, 220);
tft.println("Type EXIT in Serial to return.");
}
void drawFilesApp() {
clearScreen(ILI9341_BLACK);
tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);
tft.setTextSize(2);
tft.setCursor(10, 5);
tft.println("Files");
tft.setTextSize(1);
tft.setCursor(10, 30);
tft.println("VFS contents:");
int y = 50;
if (vfsCount == 0) {
tft.setCursor(10, y);
tft.println("(no files)");
} else {
for (int i = 0; i < vfsCount && y < tft.height() - 20; i++) {
tft.setCursor(10, y);
tft.print(vfs[i].name);
tft.print(" (");
tft.print(vfs[i].content.length());
tft.println(")");
y += 12;
}
}
tft.setCursor(10, 220);
tft.setTextColor(ILI9341_YELLOW, ILI9341_BLACK);
tft.println("Use LS/RM/CAT in terminal.");
tft.setCursor(10, 232);
tft.println("Type EXIT in Serial to return.");
}
// ===== TERMINAL RENDERING =====
void termAddLine(const String &line) {
if (termLineCount < TERM_MAX_LINES) {
termLines[termLineCount++] = line;
} else {
for (int i = 1; i < TERM_MAX_LINES; i++) {
termLines[i - 1] = termLines[i];
}
termLines[TERM_MAX_LINES - 1] = line;
}
termNeedsRedraw = true;
}
void drawTerminal() {
clearScreen(ILI9341_BLACK);
tft.setTextSize(1);
tft.setTextColor(ILI9341_GREEN, ILI9341_BLACK);
int maxVisible = tft.height() / TERM_LINE_HEIGHT - 2;
int start = max(0, termLineCount - maxVisible);
int y = 0;
for (int i = start; i < termLineCount; i++) {
tft.setCursor(0, y);
tft.print(termLines[i]);
y += TERM_LINE_HEIGHT;
}
tft.setCursor(0, y);
tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);
tft.print("> ");
tft.print(termInput);
termNeedsRedraw = false;
}
// ===== TERMINAL COMMANDS =====
void termHandleCommand(const String &cmdLine);
void handleCalcCommand(const String &cmdLine) {
// CALC a b op
String s = cmdLine;
s.trim();
int firstSpace = s.indexOf(' ');
if (firstSpace < 0) {
termAddLine("Usage: CALC <a> <b> <op>");
return;
}
String rest = s.substring(firstSpace + 1);
rest.trim();
int sp1 = rest.indexOf(' ');
if (sp1 < 0) {
termAddLine("Usage: CALC <a> <b> <op>");
return;
}
String aStr = rest.substring(0, sp1);
String rest2 = rest.substring(sp1 + 1);
rest2.trim();
int sp2 = rest2.indexOf(' ');
if (sp2 < 0) {
termAddLine("Usage: CALC <a> <b> <op>");
return;
}
String bStr = rest2.substring(0, sp2);
String opStr = rest2.substring(sp2 + 1);
opStr.trim();
double a = aStr.toDouble();
double b = bStr.toDouble();
double res = 0;
bool ok = true;
if (opStr == "+") res = a + b;
else if (opStr == "-") res = a - b;
else if (opStr == "*") res = a * b;
else if (opStr == "/") {
if (b == 0) { termAddLine("Error: divide by zero"); ok = false; }
else res = a / b;
} else {
termAddLine("Unknown op: " + opStr);
ok = false;
}
if (ok) {
calcLastExpr = aStr + " " + opStr + " " + bStr;
calcLastResult = String(res, 4);
termAddLine("= " + calcLastResult);
}
}
void termHandleCommand(const String &cmdLine) {
String cmd = cmdLine;
cmd.trim();
if (cmd.length() == 0) return;
// split first token
int sp = cmd.indexOf(' ');
String op = (sp < 0) ? cmd : cmd.substring(0, sp);
String args = (sp < 0) ? "" : cmd.substring(sp + 1);
args.trim();
op.toUpperCase();
if (op == "HELP") {
termAddLine("Commands:");
termAddLine(" HELP");
termAddLine(" LS");
termAddLine(" CAT <file>");
termAddLine(" WRITE <file>");
termAddLine(" APPEND <file>");
termAddLine(" RM <file>");
termAddLine(" INFO");
termAddLine(" CALC <a> <b> <op>");
termAddLine(" CODE_FILE (enter BIOS)");
termAddLine(" EXIT_BIOS (from BIOS OS)");
termAddLine(" EXIT (leave apps/OS)");
} else if (op == "LS") {
if (vfsCount == 0) {
termAddLine("(no files)");
} else {
for (int i = 0; i < vfsCount; i++) {
termAddLine(vfs[i].name + " (" + String(vfs[i].content.length()) + ")");
}
}
} else if (op == "CAT") {
if (args.length() == 0) {
termAddLine("Usage: CAT <file>");
} else {
int idx = vfsFindFile(args);
if (idx < 0) termAddLine("Not found: " + args);
else termAddLine(vfs[idx].content);
}
} else if (op == "WRITE") {
if (args.length() == 0) {
termAddLine("Usage: WRITE <file>");
} else {
termAddLine("Enter text, end with a single '.' line:");
String content = "";
while (true) {
while (!Serial.available()) delay(10);
String line = Serial.readStringUntil('\n');
line.trim();
if (line == ".") break;
content += line + "\n";
}
if (vfsCreateOrOverwrite(args, content)) {
termAddLine("Written: " + args);
} else {
termAddLine("VFS full, cannot write.");
}
}
} else if (op == "APPEND") {
if (args.length() == 0) {
termAddLine("Usage: APPEND <file>");
} else {
termAddLine("Enter text, end with a single '.' line:");
String content = "";
while (true) {
while (!Serial.available()) delay(10);
String line = Serial.readStringUntil('\n');
line.trim();
if (line == ".") break;
content += line + "\n";
}
if (vfsAppend(args, content)) {
termAddLine("Appended: " + args);
} else {
termAddLine("VFS full, cannot append.");
}
}
} else if (op == "RM") {
if (args.length() == 0) {
termAddLine("Usage: RM <file>");
} else {
if (vfsDelete(args)) termAddLine("Deleted: " + args);
else termAddLine("Not found: " + args);
}
} else if (op == "INFO") {
termAddLine("Tiny_OS on ESP32");
termAddLine("CPU: " + String(ESP.getChipModel()));
termAddLine("Freq: " + String(ESP.getCpuFreqMHz()) + " MHz");
termAddLine("Flash: " + String(ESP.getFlashChipSize() / (1024 * 1024)) + " MB");
termAddLine("Heap: " + String(ESP.getFreeHeap()));
} else if (op == "CALC") {
handleCalcCommand(cmd);
} else if (op == "CODE_FILE") {
uiMode = MODE_BIOS;
biosState = STATE_POST;
termAddLine("Entering BIOS flow...");
} else if (op == "EXIT_BIOS") {
uiMode = MODE_TERMINAL;
termAddLine("EXIT_BIOS: back to terminal.");
} else if (op == "EXIT") {
termAddLine("EXIT: no global effect in terminal.");
} else {
termAddLine("Unknown: " + op);
}
}
// ===== TERMINAL INPUT HANDLING =====
void handleTerminalInput() {
while (Serial.available()) {
char c = Serial.read();
if (c == '\r' || c == '\n') {
if (termInput.length() > 0) {
String cmd = termInput;
termAddLine("> " + cmd);
termInput = "";
termHandleCommand(cmd);
}
} else if (c == 8 || c == 127) {
if (termInput.length() > 0) termInput.remove(termInput.length() - 1);
termNeedsRedraw = true;
} else if (c >= 32 && c <= 126) {
if ((int)termInput.length() < TERM_COLS) {
termInput += c;
termNeedsRedraw = true;
}
}
}
if (termNeedsRedraw) drawTerminal();
}
// ===== OS SIM (WITH APP MENU) =====
void runOS() {
osSelectedApp = 0;
osInApp = false;
osRunning = true;
while (osRunning && uiMode == MODE_BIOS) {
// Check for EXIT typed in Serial to leave app or OS
if (Serial.available()) {
String s = Serial.readStringUntil('\n');
s.trim();
if (s.equalsIgnoreCase("EXIT")) {
if (osInApp) {
osInApp = false; // back to app grid
} else {
osRunning = false;
uiMode = MODE_TERMINAL;
termAddLine("EXIT: leaving OS UI, back to terminal.");
break;
}
} else if (s.equalsIgnoreCase("EXIT_BIOS")) {
osRunning = false;
uiMode = MODE_TERMINAL;
termAddLine("EXIT_BIOS: leaving BIOS/OS, back to terminal.");
break;
} else if (s.length() > 0) {
termAddLine("> " + s);
termHandleCommand(s);
}
}
if (!osInApp) {
drawOsMenu();
int dx, dy; bool enter, esc, f1, f2, f9, f10, del;
readBiosInput(dx, dy, enter, esc, f1, f2, f9, f10, del);
if (dx != 0 || dy != 0) {
int row = osSelectedApp / 3;
int col = osSelectedApp % 3;
row = constrain(row + dy, 0, 1);
col = constrain(col + dx, 0, 2);
int newIndex = row * 3 + col;
if (newIndex < OS_APP_COUNT) osSelectedApp = newIndex;
}
if (enter) {
osInApp = true;
switch (osSelectedApp) {
case OS_APP_CLOCK: drawClockApp(); break;
case OS_APP_CALENDAR: drawCalendarApp(); break;
case OS_APP_NOTES: drawNotesApp(); break;
case OS_APP_TERMINAL_INFO: drawTerminalInfoApp(); break;
case OS_APP_CALC: drawCalcApp(); break;
case OS_APP_FILES: drawFilesApp(); break;
}
}
if (esc) {
osRunning = false;
uiMode = MODE_TERMINAL;
termAddLine("ESC: leaving OS UI, back to terminal.");
break;
}
} else {
// app is drawn once; we just idle until EXIT via Serial
delay(50);
}
feedWatchdog();
checkSoftwareWatchdog();
delay(40);
}
}
// ===== MAIN LOOP DISPATCH =====
void runBiosFlow() {
switch (biosState) {
case STATE_POST:
runPost();
break;
case STATE_SETUP:
runSetup();
break;
case STATE_BOOT_MENU:
runBootMenu();
break;
case STATE_ERROR:
runError();
break;
case STATE_OS_RUNNING:
runOS();
break;
}
}
// ===== SETUP / LOOP =====
void setup() {
Serial.begin(115200);
SPI.begin(TFT_SCLK, TFT_MISO, TFT_MOSI);
tft.begin();
tft.setRotation(1);
clearScreen(ILI9341_BLACK);
termAddLine("Tiny_OS Terminal");
termAddLine("Type HELP for commands.");
drawTerminal();
loadCmos();
feedWatchdog();
}
void loop() {
if (uiMode == MODE_TERMINAL) {
handleTerminalInput();
} else if (uiMode == MODE_BIOS) {
runBiosFlow();
}
}
Loading
ili9341-cap-touch
ili9341-cap-touch