add time, button ui

This commit is contained in:
vadim committed 2026-10-27 18:35:00 +03:00
1 parent 1d7af0615e
commit dbfe5bf7cf
4 files changed
+63 -26

No files matched your search

+7 -2
View File
@@ -1,8 +1,8 @@
#pragma once #pragma once
// ---------- Wi-Fi ---------- // ---------- Wi-Fi ----------
#define WIFI_SSID "your_ssid" #define WIFI_SSID "skynet"
#define WIFI_PASSWORD "your_password" #define WIFI_PASSWORD "0218kirill"
#define WIFI_CONNECT_TIMEOUT_MS 15000 #define WIFI_CONNECT_TIMEOUT_MS 15000
// If the station connection fails, the ESP32 starts its own access point // If the station connection fails, the ESP32 starts its own access point
@@ -11,6 +11,11 @@
#define MDNS_HOSTNAME "volume" // http://volume.local #define MDNS_HOSTNAME "volume" // http://volume.local
// ---------- Time (NTP, works only in station mode) ----------
#define NTP_SERVER1 "pool.ntp.org"
#define NTP_SERVER2 "time.google.com"
#define TIME_ZONE "MSK-3" // POSIX TZ string, e.g. "CET-1CEST,M3.5.0,M10.5.0/3"
// ---------- Pins (chosen to not clash with the TFT pins in platformio.ini) ---------- // ---------- Pins (chosen to not clash with the TFT pins in platformio.ini) ----------
#define PIN_ENC_CLK 32 #define PIN_ENC_CLK 32
#define PIN_ENC_DT 33 #define PIN_ENC_DT 33
+36 -11
View File
@@ -13,18 +13,19 @@ struct Rect {
// Landscape 320x240 layout // Landscape 320x240 layout
static const int kW = 320; static const int kW = 320;
static const Rect kBar = {20, 150, 280, 24}; static const Rect kRelay = {10, 46, 120, 48};
static const Rect kBarTouch = {0, 136, 320, 50}; // larger than the bar for easier dragging static const int kClockX = 310, kClockY = 70; // right-middle of the clock text
static const Rect kRelay = {60, 190, 200, 40}; static const Rect kMinus = {10, 108, 60, 70};
static const Rect kMinus = {10, 55, 60, 70}; static const Rect kPlus = {250, 108, 60, 70};
static const Rect kPlus = {250, 55, 60, 70}; static const int kVolumeY = 120;
static const Rect kBar = {20, 196, 280, 24};
static const Rect kBarTouch = {0, 184, 320, 50}; // larger than the bar for easier dragging
static const uint16_t kBg = TFT_BLACK; static const uint16_t kBg = TFT_BLACK;
static const uint16_t kAccent = 0x3D3F; // light blue static const uint16_t kAccent = 0x3D3F; // light blue
static const uint16_t kBarBg = 0x2945; // dark grey static const uint16_t kBarBg = 0x2945; // dark grey
static const uint16_t kBtn = 0x2945; static const uint16_t kBtn = 0x2945;
static const uint16_t kBtnPressed = 0x3D3F; static const uint16_t kBtnPressed = 0x3D3F;
static const uint16_t kOn = TFT_GREEN;
static const uint16_t kOff = 0x4208; // grey static const uint16_t kOff = 0x4208; // grey
enum class TouchTarget { None, Minus, Plus, Bar, Relay }; enum class TouchTarget { None, Minus, Plus, Bar, Relay };
@@ -52,10 +53,10 @@ static void drawVolume(uint8_t volume) {
tft.setTextColor(kAccent, kBg); tft.setTextColor(kAccent, kBg);
tft.setTextDatum(TR_DATUM); tft.setTextDatum(TR_DATUM);
tft.setTextPadding(tft.textWidth("100", 7)); tft.setTextPadding(tft.textWidth("100", 7));
tft.drawNumber(volume, 200, 70, 7); tft.drawNumber(volume, 200, kVolumeY, 7);
tft.setTextPadding(0); tft.setTextPadding(0);
tft.setTextDatum(TL_DATUM); tft.setTextDatum(TL_DATUM);
tft.drawString("%", 208, 94, 4); tft.drawString("%", 208, kVolumeY + 24, 4);
int fill = kBar.w * volume / 100; int fill = kBar.w * volume / 100;
tft.fillRect(kBar.x, kBar.y, fill, kBar.h, kAccent); tft.fillRect(kBar.x, kBar.y, fill, kBar.h, kAccent);
@@ -63,11 +64,29 @@ static void drawVolume(uint8_t volume) {
} }
static void drawRelay(bool on) { static void drawRelay(bool on) {
uint16_t bg = on ? kOn : kOff; uint16_t bg = on ? kAccent : kOff;
tft.fillRoundRect(kRelay.x, kRelay.y, kRelay.w, kRelay.h, 10, bg); tft.fillRoundRect(kRelay.x, kRelay.y, kRelay.w, kRelay.h, 10, bg);
tft.setTextColor(on ? TFT_BLACK : TFT_WHITE, bg); tft.setTextColor(TFT_WHITE, bg);
tft.setTextDatum(MC_DATUM); tft.setTextDatum(MC_DATUM);
tft.drawString(on ? "POWER: ON" : "POWER: OFF", kRelay.x + kRelay.w / 2, kRelay.y + kRelay.h / 2 + 1, 4); tft.drawString(on ? "ON" : "OFF", kRelay.x + kRelay.w / 2, kRelay.y + kRelay.h / 2 + 1, 4);
}
// Returns minutes since midnight, or -1 while the time is not synced via NTP
static int currentMinutes() {
struct tm t;
if (!getLocalTime(&t, 0)) return -1;
return t.tm_hour * 60 + t.tm_min;
}
static void drawClock(int minutes) {
char buf[6];
if (minutes < 0) strcpy(buf, "--:--");
else snprintf(buf, sizeof(buf), "%02d:%02d", minutes / 60, minutes % 60);
tft.setTextColor(TFT_WHITE, kBg);
tft.setTextDatum(MR_DATUM);
tft.setTextPadding(tft.textWidth("88:88", 7));
tft.drawString(buf, kClockX, kClockY, 7);
tft.setTextPadding(0);
} }
static String networkText() { static String networkText() {
@@ -129,6 +148,7 @@ void displayTask(void *arg) {
int lastVolume = -1; int lastVolume = -1;
int lastRelay = -1; int lastRelay = -1;
String lastNet; String lastNet;
int lastMinutes = -2;
TickType_t lastNetCheck = 0; TickType_t lastNetCheck = 0;
TouchTarget active = TouchTarget::None; TouchTarget active = TouchTarget::None;
@@ -204,6 +224,11 @@ void displayTask(void *arg) {
drawNetwork(net); drawNetwork(net);
lastNet = net; lastNet = net;
} }
int minutes = currentMinutes();
if (minutes != lastMinutes) {
drawClock(minutes);
lastMinutes = minutes;
}
} }
vTaskDelay(pdMS_TO_TICKS(DISPLAY_UPDATE_MS)); vTaskDelay(pdMS_TO_TICKS(DISPLAY_UPDATE_MS));
+19 -13
View File
@@ -2,13 +2,13 @@
#include <Arduino.h> #include <Arduino.h>
static const char INDEX_HTML[] PROGMEM = R"HTML(<!doctype html> static const char INDEX_HTML[] PROGMEM = R"HTML(<!doctype html>
<html lang="ru"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Громкость</title> <title>Volume</title>
<style> <style>
:root { --bg:#111; --card:#1d1d1d; --fg:#eee; --accent:#3fa7ff; --on:#2ecc71; --off:#555; } :root { --bg:#111; --card:#1d1d1d; --fg:#eee; --accent:#3fa7ff; }
* { box-sizing:border-box; } * { box-sizing:border-box; }
body { margin:0; font-family:system-ui,sans-serif; background:var(--bg); color:var(--fg); body { margin:0; font-family:system-ui,sans-serif; background:var(--bg); color:var(--fg);
display:flex; justify-content:center; padding:24px 16px; } display:flex; justify-content:center; padding:24px 16px; }
@@ -21,22 +21,28 @@ static const char INDEX_HTML[] PROGMEM = R"HTML(<!doctype html>
button { flex:1; font-size:22px; padding:14px; border:0; border-radius:12px; button { flex:1; font-size:22px; padding:14px; border:0; border-radius:12px;
background:#2a2a2a; color:var(--fg); cursor:pointer; } background:#2a2a2a; color:var(--fg); cursor:pointer; }
button:active { transform:scale(.97); } button:active { transform:scale(.97); }
#relay { margin-top:20px; width:100%; font-size:18px; background:var(--off); } .power { display:flex; gap:4px; padding:4px; margin-bottom:20px; border-radius:14px;
#relay.on { background:var(--on); color:#000; } background:#2a2a2a; }
.power button { font-size:18px; font-weight:600; padding:10px; background:transparent;
color:#888; letter-spacing:.05em; }
.power button.active { background:var(--accent); color:#fff; }
.status { margin-top:16px; font-size:12px; opacity:.5; text-align:center; } .status { margin-top:16px; font-size:12px; opacity:.5; text-align:center; }
</style> </style>
</head> </head>
<body> <body>
<div class="card"> <div class="card">
<h1>Регулятор громкости</h1> <h1>Volume control</h1>
<div class="power">
<button id="on" onclick="relay(1)">ON</button>
<button id="off" onclick="relay(0)">OFF</button>
</div>
<div class="vol"><b id="val">--</b><span>%</span></div> <div class="vol"><b id="val">--</b><span>%</span></div>
<input id="slider" type="range" min="0" max="100" step="1"> <input id="slider" type="range" min="0" max="100" step="1">
<div class="row"> <div class="row">
<button onclick="delta(-5)">−</button> <button onclick="delta(-5)">−</button>
<button onclick="delta(5)">+</button> <button onclick="delta(5)">+</button>
</div> </div>
<button id="relay" onclick="relay()">Реле</button> <div class="status" id="status">connecting…</div>
<div class="status" id="status">подключение…</div>
</div> </div>
<script> <script>
const $ = id => document.getElementById(id); const $ = id => document.getElementById(id);
@@ -45,20 +51,20 @@ let dragging = false, sendTimer = null;
function render(s) { function render(s) {
$('val').textContent = s.volume; $('val').textContent = s.volume;
if (!dragging) $('slider').value = s.volume; if (!dragging) $('slider').value = s.volume;
$('relay').classList.toggle('on', s.relay); $('on').classList.toggle('active', s.relay);
$('relay').textContent = s.relay ? 'Реле: ВКЛ' : 'Реле: ВЫКЛ'; $('off').classList.toggle('active', !s.relay);
$('status').textContent = 'онлайн'; $('status').textContent = 'online';
} }
async function api(path, method = 'GET') { async function api(path, method = 'GET') {
try { try {
const r = await fetch(path, { method }); const r = await fetch(path, { method });
if (r.ok) render(await r.json()); if (r.ok) render(await r.json());
} catch (e) { $('status').textContent = 'нет связи'; } } catch (e) { $('status').textContent = 'offline'; }
} }
const delta = d => api('/api/volume?delta=' + d, 'POST'); const delta = d => api('/api/volume?delta=' + d, 'POST');
const relay = () => api('/api/relay?state=toggle', 'POST'); const relay = on => api('/api/relay?state=' + (on ? 'on' : 'off'), 'POST');
const slider = $('slider'); const slider = $('slider');
slider.addEventListener('pointerdown', () => dragging = true); slider.addEventListener('pointerdown', () => dragging = true);
+1
View File
@@ -59,6 +59,7 @@ static void startWifi() {
if (WiFi.status() == WL_CONNECTED) { if (WiFi.status() == WL_CONNECTED) {
Serial.printf("[web] connected, IP: %s\n", WiFi.localIP().toString().c_str()); Serial.printf("[web] connected, IP: %s\n", WiFi.localIP().toString().c_str());
configTzTime(TIME_ZONE, NTP_SERVER1, NTP_SERVER2);
} else { } else {
WiFi.disconnect(true); WiFi.disconnect(true);
WiFi.mode(WIFI_AP); WiFi.mode(WIFI_AP);