add time, button ui
This commit is contained in:
1 parent
1d7af0615e
commit
dbfe5bf7cf
4 files changed
+63
-26
No files matched your search
+7
-2
@@ -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
@@ -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
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in new issue
Block a user