Загрузка данных
#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>
// =====================================================
// HOSAN DEVICE V20 PRO
// ESP32 DOIT DEVKIT V1
// =====================================================
// ---------- TFT ----------
#define TFT_CS 5
#define TFT_DC 2
#define TFT_RST 4
#define TFT_BL 16
// ---------- ENCODER ----------
#define ENC_A 32
#define ENC_B 33
#define ENC_SW 25
Adafruit_ST7735 tft(TFT_CS, TFT_DC, TFT_RST);
WebServer server(80);
// ---------- WiFi ----------
const char* WIFI_NAME = "HOSAN DEVICE";
const char* WIFI_PASS = "hosan2026";
// =====================================================
// EVENTS
// Префиксы нужны, чтобы никакие системные имена ESP32
// не конфликтовали с нашими переменными.
// =====================================================
enum EncoderEvent {
EV_NONE,
EV_CW,
EV_CCW,
EV_PRESS,
EV_LONG
};
// =====================================================
// PAGES
// =====================================================
enum DevicePage {
PG_HOME,
PG_MENU,
PG_CLOCK,
PG_STOPWATCH,
PG_TIMER,
PG_RANDOM,
PG_WIFI_SCAN,
PG_WIFI_INFO,
PG_SYSTEM,
PG_GPIO,
PG_ENCODER,
PG_DISPLAY,
PG_BACKLIGHT,
PG_UPTIME,
PG_POMODORO,
PG_DICE,
PG_REACTION,
PG_BLE
};
DevicePage currentPage = PG_HOME;
// =====================================================
// MENU
// =====================================================
const char* menuItems[] = {
"CLOCK",
"STOPWATCH",
"TIMER",
"RANDOM",
"WIFI SCAN",
"WIFI INFO",
"SYSTEM",
"GPIO TEST",
"ENCODER TEST",
"DISPLAY TEST",
"BACKLIGHT",
"UPTIME",
"POMODORO",
"DICE",
"REACTION",
"BLE STATUS"
};
const int MENU_COUNT =
sizeof(menuItems) / sizeof(menuItems[0]);
int menuIndex = 0;
// =====================================================
// ENCODER STATE
// =====================================================
uint8_t encoderPrevious = 0;
int encoderAccumulator = 0;
bool buttonStable = HIGH;
bool buttonRaw = HIGH;
unsigned long buttonChangedAt = 0;
unsigned long buttonPressedAt = 0;
// =====================================================
// TIMER / STOPWATCH
// =====================================================
bool stopwatchRunning = false;
unsigned long stopwatchStarted = 0;
unsigned long stopwatchSaved = 0;
bool timerRunning = false;
unsigned long timerEpoch = 0;
unsigned long timerLength = 60;
// =====================================================
// POMODORO
// =====================================================
bool pomodoroRunning = false;
unsigned long pomodoroStarted = 0;
const unsigned long POMODORO_LENGTH = 25UL * 60UL;
// =====================================================
// REACTION
// =====================================================
bool reactionWaiting = false;
unsigned long reactionTarget = 0;
// =====================================================
// DISPLAY HELPERS
// =====================================================
void clearDisplay() {
tft.fillScreen(ST77XX_BLACK);
}
void drawText(
int x,
int y,
const String& value,
uint16_t color = ST77XX_WHITE,
uint8_t size = 1
) {
tft.setTextColor(color);
tft.setTextSize(size);
tft.setCursor(x, y);
tft.print(value);
}
void drawCenter(
int y,
const String& value,
uint16_t color = ST77XX_WHITE,
uint8_t size = 1
) {
int width = value.length() * 6 * size;
int x = (160 - width) / 2;
if (x < 0) {
x = 0;
}
drawText(x, y, value, color, size);
}
void drawHeader(const String& title) {
clearDisplay();
drawText(
3,
2,
"HOSAN",
ST77XX_CYAN,
1
);
drawText(
108,
2,
title,
ST77XX_WHITE,
1
);
tft.drawFastHLine(
0,
14,
160,
ST77XX_CYAN
);
}
// =====================================================
// SPLASH
// =====================================================
void showSplash() {
clearDisplay();
int x = 80;
// Верхняя стрелка
tft.fillTriangle(
x,
18,
x - 9,
34,
x + 9,
34,
ST77XX_WHITE
);
// Вертикальная линия
tft.fillRect(
x - 2,
34,
4,
22,
ST77XX_WHITE
);
// Горизонтальная линия
tft.fillRect(
42,
53,
76,
6,
ST77XX_WHITE
);
// Круг
tft.drawCircle(
x,
56,
9,
ST77XX_WHITE
);
tft.fillCircle(
x,
56,
2,
ST77XX_BLACK
);
// Нижняя стрелка
tft.fillTriangle(
x,
94,
x - 9,
78,
x + 9,
78,
ST77XX_WHITE
);
drawCenter(
108,
"HOSAN DEVICE",
ST77XX_WHITE,
1
);
drawCenter(
121,
"V20 PRO",
ST77XX_CYAN,
1
);
}
// =====================================================
// ENCODER
// =====================================================
EncoderEvent readEncoder() {
EncoderEvent result = EV_NONE;
uint8_t current =
(digitalRead(ENC_A) << 1) |
digitalRead(ENC_B);
static const int8_t table[16] = {
0, -1, 1, 0,
1, 0, 0, -1,
-1, 0, 0, 1,
0, 1, -1, 0
};
if (current != encoderPrevious) {
int index =
(encoderPrevious << 2) |
current;
encoderAccumulator += table[index];
encoderPrevious = current;
// Полный detent энкодера.
// Именно здесь фиксируем вращение.
if (encoderAccumulator >= 4) {
encoderAccumulator = 0;
result = EV_CW;
}
else if (encoderAccumulator <= -4) {
encoderAccumulator = 0;
result = EV_CCW;
}
}
// ---------- BUTTON ----------
unsigned long now = millis();
bool raw = digitalRead(ENC_SW);
if (raw != buttonRaw) {
buttonRaw = raw;
buttonChangedAt = now;
}
if (
now - buttonChangedAt >= 35 &&
raw != buttonStable
) {
buttonStable = raw;
// Нажали
if (raw == LOW) {
buttonPressedAt = now;
}
// Отпустили
else {
if (buttonPressedAt != 0) {
unsigned long duration =
now - buttonPressedAt;
buttonPressedAt = 0;
if (duration >= 700) {
result = EV_LONG;
}
else {
result = EV_PRESS;
}
}
}
}
return result;
}
// =====================================================
// INITIALIZE ENCODER
// =====================================================
void setupEncoder() {
pinMode(
ENC_A,
INPUT_PULLUP
);
pinMode(
ENC_B,
INPUT_PULLUP
);
pinMode(
ENC_SW,
INPUT_PULLUP
);
encoderPrevious =
(digitalRead(ENC_A) << 1) |
digitalRead(ENC_B);
buttonStable =
digitalRead(ENC_SW);
buttonRaw =
buttonStable;
buttonChangedAt = millis();
}
// =====================================================
// HOME
// =====================================================
void drawHome() {
drawHeader("HOME");
drawCenter(
30,
"READY",
ST77XX_GREEN,
2
);
drawCenter(
58,
"HOSAN DEVICE",
ST77XX_CYAN,
1
);
drawText(
5,
82,
"IP: " +
WiFi.softAPIP().toString()
);
drawText(
5,
98,
"RAM: " +
String(ESP.getFreeHeap())
);
drawText(
5,
114,
"PRESS = MENU"
);
}
// =====================================================
// MENU
// =====================================================
void drawMenu() {
drawHeader("TOOLS");
int first =
menuIndex - 2;
if (first < 0) {
first = 0;
}
if (first > MENU_COUNT - 5) {
first = MENU_COUNT - 5;
}
if (first < 0) {
first = 0;
}
for (int i = 0; i < 5; i++) {
int index =
first + i;
if (index >= MENU_COUNT) {
break;
}
int y =
22 + i * 18;
if (index == menuIndex) {
tft.fillRoundRect(
2,
y - 2,
156,
16,
3,
ST77XX_CYAN
);
drawText(
7,
y,
String(index + 1) +
" " +
menuItems[index],
ST77XX_BLACK,
1
);
}
else {
drawText(
7,
y,
String(index + 1) +
" " +
menuItems[index],
ST77XX_WHITE,
1
);
}
}
}
// =====================================================
// CLOCK
// =====================================================
void drawClockPage() {
drawHeader("CLOCK");
unsigned long seconds =
millis() / 1000UL;
unsigned long h =
(seconds / 3600UL) % 24UL;
unsigned long m =
(seconds / 60UL) % 60UL;
unsigned long s =
seconds % 60UL;
char buffer[16];
sprintf(
buffer,
"%02lu:%02lu:%02lu",
h,
m,
s
);
drawCenter(
45,
buffer,
ST77XX_WHITE,
2
);
drawCenter(
80,
"UPTIME",
ST77XX_CYAN
);
drawCenter(
98,
String(seconds) + " SEC",
ST77XX_GREEN
);
drawCenter(
118,
"LONG = BACK",
ST77XX_YELLOW
);
}
// =====================================================
// STOPWATCH
// =====================================================
void drawStopwatch() {
drawHeader("STOP");
unsigned long elapsed =
stopwatchSaved;
if (stopwatchRunning) {
elapsed +=
(millis() - stopwatchStarted)
/ 1000UL;
}
unsigned long h =
elapsed / 3600UL;
unsigned long m =
(elapsed / 60UL) % 60UL;
unsigned long s =
elapsed % 60UL;
char buffer[16];
sprintf(
buffer,
"%02lu:%02lu:%02lu",
h,
m,
s
);
drawCenter(
45,
buffer,
ST77XX_WHITE,
2
);
drawCenter(
80,
stopwatchRunning
? "RUNNING"
: "PAUSED",
ST77XX_GREEN
);
drawCenter(
105,
"PRESS START/PAUSE",
ST77XX_YELLOW
);
}
// =====================================================
// TIMER
// =====================================================
void drawTimer() {
drawHeader("TIMER");
unsigned long elapsed = 0;
if (timerRunning) {
elapsed =
(millis() - timerEpoch)
/ 1000UL;
}
unsigned long remaining = 0;
if (elapsed < timerLength) {
remaining =
timerLength - elapsed;
}
unsigned long minutes =
remaining / 60UL;
unsigned long seconds =
remaining % 60UL;
char buffer[12];
sprintf(
buffer,
"%02lu:%02lu",
minutes,
seconds
);
uint16_t color =
remaining == 0
? ST77XX_RED
: ST77XX_WHITE;
drawCenter(
42,
buffer,
color,
2
);
drawCenter(
72,
"ROTATE = MIN",
ST77XX_CYAN
);
drawCenter(
92,
timerRunning
? "RUNNING"
: "READY",
ST77XX_GREEN
);
drawCenter(
112,
"PRESS = START",
ST77XX_YELLOW
);
if (
timerRunning &&
remaining == 0
) {
timerRunning = false;
}
}
// =====================================================
// RANDOM
// =====================================================
void drawRandom() {
drawHeader("RANDOM");
uint32_t value =
esp_random();
drawCenter(
45,
String(value),
ST77XX_WHITE
);
drawCenter(
80,
"HARDWARE RNG",
ST77XX_CYAN
);
drawCenter(
105,
"PRESS = NEW",
ST77XX_YELLOW
);
}
// =====================================================
// WIFI INFO
// =====================================================
void drawWifiInfo() {
drawHeader("WIFI");
drawText(
4,
25,
"AP: " +
String(WIFI_NAME)
);
drawText(
4,
43,
"IP: " +
WiFi.softAPIP().toString()
);
drawText(
4,
61,
"CH: " +
String(WiFi.channel())
);
drawText(
4,
79,
"MAC:"
);
drawText(
4,
94,
WiFi.softAPmacAddress()
);
drawText(
4,
112,
"CLIENTS: " +
String(WiFi.softAPgetStationNum())
);
}
// =====================================================
// WIFI SCAN
// =====================================================
void drawWifiScan() {
drawHeader("SCAN");
int count =
WiFi.scanComplete();
if (count == WIFI_SCAN_RUNNING) {
drawCenter(
55,
"SCANNING...",
ST77XX_CYAN
);
return;
}
if (count < 0) {
WiFi.scanNetworks(
true,
true
);
drawCenter(
55,
"STARTING...",
ST77XX_CYAN
);
return;
}
if (count == 0) {
drawCenter(
55,
"NO NETWORKS",
ST77XX_RED
);
WiFi.scanDelete();
return;
}
int visible =
min(count, 5);
for (int i = 0; i < visible; i++) {
String ssid =
WiFi.SSID(i);
if (ssid.length() > 16) {
ssid =
ssid.substring(0, 16);
}
int y =
22 + i * 18;
drawText(
3,
y,
ssid
);
drawText(
118,
y,
String(WiFi.RSSI(i))
);
}
WiFi.scanDelete();
}
// =====================================================
// SYSTEM
// =====================================================
void drawSystem() {
drawHeader("SYSTEM");
drawText(
4,
24,
"CPU: " +
String(ESP.getCpuFreqMHz()) +
" MHz"
);
drawText(
4,
42,
"HEAP: " +
String(ESP.getFreeHeap())
);
drawText(
4,
60,
"MIN: " +
String(ESP.getMinFreeHeap())
);
drawText(
4,
78,
"FLASH: " +
String(
ESP.getFlashChipSize() /
1024
) +
" KB"
);
drawText(
4,
96,
"CORES: " +
String(ESP.getChipCores())
);
drawText(
4,
114,
"SDK: " +
String(ESP.getSdkVersion())
);
}
// =====================================================
// GPIO TEST
// =====================================================
void drawGPIO() {
drawHeader("GPIO");
drawText(
5,
27,
"ENC A: " +
String(digitalRead(ENC_A))
);
drawText(
5,
45,
"ENC B: " +
String(digitalRead(ENC_B))
);
drawText(
5,
63,
"BUTTON: " +
String(digitalRead(ENC_SW))
);
drawText(
5,
81,
"TFT BL: " +
String(digitalRead(TFT_BL))
);
drawText(
5,
105,
"INPUT DIAGNOSTIC",
ST77XX_GREEN
);
}
// =====================================================
// ENCODER TEST
// =====================================================
void drawEncoderTest() {
drawHeader("ENCODER");
drawText(
5,
28,
"A: " +
String(digitalRead(ENC_A))
);
drawText(
5,
46,
"B: " +
String(digitalRead(ENC_B))
);
drawText(
5,
64,
"SW: " +
String(digitalRead(ENC_SW))
);
drawCenter(
94,
"ROTATE / PRESS",
ST77XX_GREEN
);
drawCenter(
112,
"LONG = BACK",
ST77XX_YELLOW
);
}
// =====================================================
// DISPLAY TEST
// =====================================================
void drawDisplayTest() {
drawHeader("DISPLAY");
tft.drawRect(
3,
22,
154,
70,
ST77XX_RED
);
tft.drawLine(
3,
22,
157,
92,
ST77XX_GREEN
);
tft.drawLine(
157,
22,
3,
92,
ST77XX_CYAN
);
drawCenter(
105,
"TFT OK",
ST77XX_WHITE
);
}
// =====================================================
// BACKLIGHT
// =====================================================
void drawBacklight() {
drawHeader("BACKLIGHT");
drawCenter(
42,
"GPIO 16",
ST77XX_CYAN,
2
);
drawCenter(
72,
"BACKLIGHT ON",
ST77XX_GREEN
);
drawCenter(
102,
"WEB CONTROL",
ST77XX_WHITE
);
}
// =====================================================
// UPTIME
// =====================================================
void drawUptime() {
drawHeader("UPTIME");
unsigned long sec =
millis() / 1000UL;
drawCenter(
45,
String(sec) + " SEC",
ST77XX_WHITE,
2
);
drawCenter(
82,
String(sec / 60UL) + " MIN",
ST77XX_GREEN
);
}
// =====================================================
// POMODORO
// =====================================================
void drawPomodoro() {
drawHeader("POMODORO");
unsigned long elapsed = 0;
if (pomodoroRunning) {
elapsed =
(millis() - pomodoroStarted)
/ 1000UL;
}
unsigned long remaining = 0;
if (elapsed < POMODORO_LENGTH) {
remaining =
POMODORO_LENGTH - elapsed;
}
unsigned long min =
remaining / 60UL;
unsigned long sec =
remaining % 60UL;
char buffer[12];
sprintf(
buffer,
"%02lu:%02lu",
min,
sec
);
drawCenter(
45,
buffer,
ST77XX_WHITE,
2
);
drawCenter(
80,
pomodoroRunning
? "FOCUS"
: "READY",
ST77XX_GREEN
);
}
// =====================================================
// DICE
// =====================================================
void drawDice() {
drawHeader("DICE");
int value =
random(1, 7);
drawCenter(
42,
String(value),
ST77XX_WHITE,
4
);
drawCenter(
105,
"PRESS = ROLL",
ST77XX_YELLOW
);
}
// =====================================================
// REACTION
// =====================================================
void drawReaction() {
drawHeader("REACTION");
if (reactionWaiting) {
drawCenter(
48,
"PRESS!",
ST77XX_GREEN,
2
);
}
else {
drawCenter(
48,
"WAIT...",
ST77XX_YELLOW,
2
);
}
}
// =====================================================
// BLE STATUS
// =====================================================
void drawBLE() {
drawHeader("BLE");
drawCenter(
45,
"BLE READY",
ST77XX_CYAN,
2
);
drawCenter(
76,
"SAFE MODE",
ST77XX_GREEN
);
drawCenter(
102,
"DIAGNOSTICS",
ST77XX_WHITE
);
}
// =====================================================
// PAGE DRAW
// =====================================================
void renderPage() {
switch (currentPage) {
case PG_HOME:
drawHome();
break;
case PG_MENU:
drawMenu();
break;
case PG_CLOCK:
drawClockPage();
break;
case PG_STOPWATCH:
drawStopwatch();
break;
case PG_TIMER:
drawTimer();
break;
case PG_RANDOM:
drawRandom();
break;
case PG_WIFI_SCAN:
drawWifiScan();
break;
case PG_WIFI_INFO:
drawWifiInfo();
break;
case PG_SYSTEM:
drawSystem();
break;
case PG_GPIO:
drawGPIO();
break;
case PG_ENCODER:
drawEncoderTest();
break;
case PG_DISPLAY:
drawDisplayTest();
break;
case PG_BACKLIGHT:
drawBacklight();
break;
case PG_UPTIME:
drawUptime();
break;
case PG_POMODORO:
drawPomodoro();
break;
case PG_DICE:
drawDice();
break;
case PG_REACTION:
drawReaction();
break;
case PG_BLE:
drawBLE();
break;
default:
currentPage = PG_HOME;
drawHome();
break;
}
}
// =====================================================
// OPEN MENU ITEM
// =====================================================
void openSelected() {
switch (menuIndex) {
case 0:
currentPage = PG_CLOCK;
break;
case 1:
currentPage = PG_STOPWATCH;
break;
case 2:
currentPage = PG_TIMER;
break;
case 3:
currentPage = PG_RANDOM;
break;
case 4:
currentPage = PG_WIFI_SCAN;
WiFi.scanNetworks(
true,
true
);
break;
case 5:
currentPage = PG_WIFI_INFO;
break;
case 6:
currentPage = PG_SYSTEM;
break;
case 7:
currentPage = PG_GPIO;
break;
case 8:
currentPage = PG_ENCODER;
break;
case 9:
currentPage = PG_DISPLAY;
break;
case 10:
currentPage = PG_BACKLIGHT;
break;
case 11:
currentPage = PG_UPTIME;
break;
case 12:
currentPage = PG_POMODORO;
break;
case 13:
currentPage = PG_DICE;
break;
case 14:
currentPage = PG_REACTION;
reactionWaiting = false;
reactionTarget =
millis() +
random(1500, 4000);
break;
case 15:
currentPage = PG_BLE;
break;
default:
currentPage = PG_HOME;
break;
}
renderPage();
}
// =====================================================
// ENCODER ACTION
// =====================================================
void handleEvent(
EncoderEvent event
) {
// HOME
if (currentPage == PG_HOME) {
if (event == EV_PRESS) {
currentPage = PG_MENU;
renderPage();
}
return;
}
// MENU
if (currentPage == PG_MENU) {
if (event == EV_CW) {
menuIndex++;
if (menuIndex >= MENU_COUNT) {
menuIndex = 0;
}
renderPage();
}
else if (event == EV_CCW) {
menuIndex--;
if (menuIndex < 0) {
menuIndex = MENU_COUNT - 1;
}
renderPage();
}
else if (event == EV_PRESS) {
openSelected();
}
return;
}
// LONG PRESS = BACK
if (event == EV_LONG) {
currentPage = PG_MENU;
renderPage();
return;
}
// STOPWATCH
if (
currentPage == PG_STOPWATCH &&
event == EV_PRESS
) {
if (stopwatchRunning) {
stopwatchSaved +=
(millis() - stopwatchStarted)
/ 1000UL;
stopwatchRunning = false;
}
else {
stopwatchStarted =
millis();
stopwatchRunning = true;
}
renderPage();
return;
}
// TIMER
if (currentPage == PG_TIMER) {
if (event == EV_CW) {
timerLength += 60;
if (timerLength > 59940) {
timerLength = 59940;
}
renderPage();
}
else if (event == EV_CCW) {
if (timerLength > 60) {
timerLength -= 60;
}
renderPage();
}
else if (event == EV_PRESS) {
timerEpoch =
millis();
timerRunning = true;
renderPage();
}
return;
}
// RANDOM
if (
currentPage == PG_RANDOM &&
event == EV_PRESS
) {
renderPage();
return;
}
// DICE
if (
currentPage == PG_DICE &&
event == EV_PRESS
) {
renderPage();
return;
}
// POMODORO
if (
currentPage == PG_POMODORO &&
event == EV_PRESS
) {
if (pomodoroRunning) {
pomodoroRunning = false;
}
else {
pomodoroStarted =
millis();
pomodoroRunning = true;
}
renderPage();
return;
}
// REACTION
if (
currentPage == PG_REACTION &&
event == EV_PRESS
) {
if (reactionWaiting) {
unsigned long result =
millis() - reactionTarget;
drawHeader("RESULT");
drawCenter(
48,
String(result) + " ms",
ST77XX_GREEN,
2
);
drawCenter(
90,
"REACTION TIME",
ST77XX_CYAN
);
reactionWaiting = false;
}
return;
}
}
// =====================================================
// WEB INTERFACE
// =====================================================
const char WEB_PAGE[] PROGMEM = R"HTML(
<!DOCTYPE html>
<html>
<head>
<meta name="viewport"
content="width=device-width,initial-scale=1">
<title>HOSAN DEVICE V20 PRO</title>
<style>
* {
box-sizing:border-box;
}
body {
margin:0;
background:
radial-gradient(
circle at top,
#10202b,
#05080c 60%
);
color:#f4f7f9;
font-family:
system-ui,
Arial,
sans-serif;
}
main {
max-width:720px;
margin:auto;
padding:16px;
}
.hero {
padding:22px;
border-radius:24px;
border:1px solid #263944;
background:#0b1218;
box-shadow:
0 15px 40px #0008;
}
h1 {
margin:0;
color:#00e5ff;
letter-spacing:6px;
}
.sub {
color:#71818d;
margin-top:5px;
}
.card {
margin-top:12px;
padding:16px;
border-radius:20px;
background:#0c141b;
border:1px solid #21303a;
}
.grid {
display:grid;
grid-template-columns:
repeat(2,1fr);
gap:8px;
margin-top:10px;
}
button {
border:0;
border-radius:13px;
padding:14px;
font-weight:800;
background:#e8f0f3;
color:#071017;
}
button.dark {
background:#14232c;
color:#bdefff;
border:1px solid #28404c;
}
.stat {
color:#00e5ff;
font-size:22px;
}
pre {
white-space:pre-wrap;
word-break:break-word;
color:#9fefff;
}
@media(max-width:500px) {
.grid {
grid-template-columns:1fr;
}
}
</style>
</head>
<body>
<main>
<div class="hero">
<h1>HOSAN</h1>
<div class="sub">
DEVICE V20 PRO · ESP32
</div>
<p id="status">
Connecting...
</p>
</div>
<div class="card">
<b>REMOTE ENCODER</b>
<div class="grid">
<button onclick="cmd('PREV')">
◀ PREVIOUS
</button>
<button onclick="cmd('NEXT')">
NEXT ▶
</button>
<button onclick="cmd('OK')">
OK
</button>
<button onclick="cmd('BACK')">
BACK
</button>
</div>
</div>
<div class="card">
<b>TOOLS</b>
<div class="grid">
<button onclick="openPage('CLOCK')">
CLOCK
</button>
<button onclick="openPage('STOP')">
STOPWATCH
</button>
<button onclick="openPage('TIMER')">
TIMER
</button>
<button onclick="openPage('RANDOM')">
RANDOM
</button>
<button onclick="openPage('SCAN')">
WIFI SCAN
</button>
<button onclick="openPage('WIFI')">
WIFI INFO
</button>
<button onclick="openPage('SYSTEM')">
SYSTEM
</button>
<button onclick="openPage('ENCODER')">
ENCODER
</button>
<button onclick="openPage('DISPLAY')">
DISPLAY
</button>
<button onclick="openPage('POMO')">
POMODORO
</button>
<button onclick="openPage('DICE')">
DICE
</button>
<button onclick="openPage('UPTIME')">
UPTIME
</button>
</div>
</div>
<div class="card">
<b>LIVE SYSTEM</b>
<pre id="data">
Loading...
</pre>
</div>
</main>
<script>
async function cmd(x) {
await fetch(
'/cmd?x=' +
encodeURIComponent(x)
);
}
async function openPage(x) {
await fetch(
'/open?x=' +
encodeURIComponent(x)
);
}
async function update() {
try {
const response =
await fetch('/api/status');
const data =
await response.json();
document.getElementById(
'status'
).textContent =
'IP ' +
data.ip +
' · RAM ' +
data.heap +
' B';
document.getElementById(
'data'
).textContent =
JSON.stringify(
data,
null,
2
);
}
catch(e) {
document.getElementById(
'status'
).textContent =
'Connection lost';
}
}
setInterval(
update,
1000
);
update();
</script>
</body>
</html>
)HTML";
// =====================================================
// WEB COMMAND
// =====================================================
void executeCommand(
const String& command
) {
if (command == "NEXT") {
handleEvent(EV_CW);
}
else if (command == "PREV") {
handleEvent(EV_CCW);
}
else if (command == "OK") {
handleEvent(EV_PRESS);
}
else if (command == "BACK") {
handleEvent(EV_LONG);
}
}
// =====================================================
// DIRECT WEB OPEN
// =====================================================
void openFromWeb(
const String& command
) {
if (command == "CLOCK") {
currentPage = PG_CLOCK;
}
else if (command == "STOP") {
currentPage = PG_STOPWATCH;
}
else if (command == "TIMER") {
currentPage = PG_TIMER;
}
else if (command == "RANDOM") {
currentPage = PG_RANDOM;
}
else if (command == "SCAN") {
currentPage = PG_WIFI_SCAN;
WiFi.scanNetworks(
true,
true
);
}
else if (command == "WIFI") {
currentPage = PG_WIFI_INFO;
}
else if (command == "SYSTEM") {
currentPage = PG_SYSTEM;
}
else if (command == "ENCODER") {
currentPage = PG_ENCODER;
}
else if (command == "DISPLAY") {
currentPage = PG_DISPLAY;
}
else if (command == "POMO") {
currentPage = PG_POMODORO;
}
else if (command == "DICE") {
currentPage = PG_DICE;
}
else if (command == "UPTIME") {
currentPage = PG_UPTIME;
}
renderPage();
}
// =====================================================
// WEB SERVER
// =====================================================
void setupWeb() {
WiFi.mode(WIFI_AP);
WiFi.softAP(
WIFI_NAME,
WIFI_PASS
);
server.on(
"/",
[]() {
server.send_P(
200,
"text/html",
WEB_PAGE
);
}
);
server.on(
"/cmd",
[]() {
if (
server.hasArg("x")
) {
executeCommand(
server.arg("x")
);
}
server.send(
200,
"text/plain",
"OK"
);
}
);
server.on(
"/open",
[]() {
if (
server.hasArg("x")
) {
openFromWeb(
server.arg("x")
);
}
server.send(
200,
"text/plain",
"OK"
);
}
);
server.on(
"/api/status",
[]() {
String json = "{";
json +=
"\"device\":\"HOSAN DEVICE V20 PRO\",";
json +=
"\"ip\":\"" +
WiFi.softAPIP().toString() +
"\",";
json +=
"\"channel\":" +
String(WiFi.channel()) +
",";
json +=
"\"clients\":" +
String(
WiFi.softAPgetStationNum()
) +
",";
json +=
"\"heap\":" +
String(
ESP.getFreeHeap()
) +
",";
json +=
"\"minHeap\":" +
String(
ESP.getMinFreeHeap()
) +
",";
json +=
"\"uptime\":" +
String(
millis() / 1000UL
) +
",";
json +=
"\"cpuMHz\":" +
String(
ESP.getCpuFreqMHz()
) +
",";
json +=
"\"flashKB\":" +
String(
ESP.getFlashChipSize() /
1024
);
json += "}";
server.send(
200,
"application/json",
json
);
}
);
server.begin();
}
// =====================================================
// SETUP
// =====================================================
void setup() {
Serial.begin(115200);
// TFT BACKLIGHT
pinMode(
TFT_BL,
OUTPUT
);
digitalWrite(
TFT_BL,
HIGH
);
// TFT
tft.initR(
INITR_BLACKTAB
);
tft.setRotation(1);
// ENCODER
setupEncoder();
// Random
randomSeed(
esp_random()
);
// Splash
showSplash();
delay(800);
// WiFi
setupWeb();
// Home
currentPage =
PG_HOME;
renderPage();
}
// =====================================================
// LOOP
// =====================================================
void loop() {
server.handleClient();
EncoderEvent event =
readEncoder();
if (event != EV_NONE) {
handleEvent(event);
}
// Reaction timer
if (
currentPage == PG_REACTION &&
!reactionWaiting &&
millis() >= reactionTarget
) {
reactionWaiting = true;
renderPage();
}
// Dynamic pages
static unsigned long lastDraw = 0;
if (
millis() - lastDraw >= 250
) {
lastDraw = millis();
if (
currentPage == PG_CLOCK ||
currentPage == PG_STOPWATCH ||
currentPage == PG_TIMER ||
currentPage == PG_UPTIME ||
currentPage == PG_POMODORO
) {
renderPage();
}
}
delay(5);
}