Написал небольшую библиотеку для автоматической настройки системных часов esp. Использует сервис геолокации, получает данные и время и настраивает NTP-сервера. После однократного выполнения в блокирующем режиме экземпляр класса можно удалить, он не нужен для дальнейшей работы.
README.md не дописан. Дополню в ближайшее время.
ВНИМАНИЕ!! Не работает если сервис не сможет правильно определить ваш ip!!! Прошу отнестись с пониманием.
P/S/ Может не работать при очередных сюрпризах наших надзорных органов
Тут уж я бессилен.
зря вы так, надо сразу пилить только офлайн версию, вот с настройкой времени от телефона)))
#include <WiFi.h>
#include <WebServer.h>
#include <EEPROM.h>
#include <time.h>
const char* ap_ssid = "ESP32_TimeServer";
const char* ap_password = "12345678";
WebServer server(80);
struct TimeData {
unsigned long timestamp;
unsigned long lastSaveMillis;
uint32_t crc32;
};
TimeData savedTime;
bool timeInitialized = false;
uint32_t calculateCRC32(const uint8_t *data, size_t length);
void saveTimeToEEPROM();
bool loadTimeFromEEPROM();
unsigned long getCurrentUnixTime();
String unixTimeToString(unsigned long unixTime);
String getHTMLPage();
void handleRoot();
void handleGetTime();
void handleSetTime();
uint32_t calculateCRC32(const uint8_t *data, size_t length) {
uint32_t crc = 0xffffffff;
for (size_t i = 0; i < length; i++) {
crc ^= (uint32_t)data[i] << 24;
for (int j = 0; j < 8; j++) {
if (crc & 0x80000000) {
crc = (crc << 1) ^ 0x04C11DB7;
} else {
crc <<= 1;
}
}
}
return crc;
}
void saveTimeToEEPROM() {
if (!timeInitialized) return;
savedTime.timestamp = getCurrentUnixTime();
savedTime.lastSaveMillis = millis();
savedTime.crc32 = calculateCRC32((uint8_t*)&savedTime, sizeof(savedTime) - sizeof(savedTime.crc32));
EEPROM.put(0, savedTime);
if (EEPROM.commit()) {
Serial.println("✓ Время сохранено в EEPROM: " + String(savedTime.timestamp) +
" (" + unixTimeToString(savedTime.timestamp) + ")");
} else {
Serial.println("✗ Ошибка сохранения в EEPROM!");
}
}
bool loadTimeFromEEPROM() {
EEPROM.get(0, savedTime);
uint32_t calculatedCRC = calculateCRC32((uint8_t*)&savedTime, sizeof(savedTime) - sizeof(savedTime.crc32));
bool isValidTime = (savedTime.timestamp > 1609459200) && (savedTime.timestamp < 1893456000);
if (savedTime.crc32 == calculatedCRC && isValidTime) {
timeInitialized = true;
Serial.println("✓ Время загружено из EEPROM: " + String(savedTime.timestamp) +
" (" + unixTimeToString(savedTime.timestamp) + ")");
Serial.println(" Сохранено при millis(): " + String(savedTime.lastSaveMillis));
Serial.println(" Текущий millis(): " + String(millis()));
return true;
}
savedTime.timestamp = 0;
savedTime.lastSaveMillis = 0;
savedTime.crc32 = 0;
timeInitialized = false;
Serial.println("✗ EEPROM содержит невалидные данные или пуста");
return false;
}
unsigned long getCurrentUnixTime() {
if (!timeInitialized) return 0;
if (millis() < savedTime.lastSaveMillis) {
return savedTime.timestamp + (millis() / 1000);
} else {
return savedTime.timestamp + ((millis() - savedTime.lastSaveMillis) / 1000);
}
}
String unixTimeToString(unsigned long unixTime) {
if (unixTime == 0 || unixTime < 1609459200) return "Время не установлено";
time_t rawtime = unixTime;
struct tm *timeinfo;
timeinfo = localtime(&rawtime);
timeinfo->tm_hour += 3;
mktime(timeinfo);
char buffer[30];
strftime(buffer, sizeof(buffer), "%d.%m.%Y %H:%M:%S", timeinfo);
return String(buffer);
}
String getHTMLPage() {
String html = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ESP32 Smart Clock</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
text-align: center;
padding: 20px;
background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%);
color: white;
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.container {
background: rgba(255, 255, 255, 0.15);
padding: 30px;
border-radius: 20px;
backdrop-filter: blur(15px);
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.2);
max-width: 500px;
width: 90%;
border: 1px solid rgba(255, 255, 255, 0.2);
}
h1 {
font-size: 32px;
margin-bottom: 10px;
color: #fff;
text-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
}
.subtitle {
font-size: 16px;
opacity: 0.9;
margin-bottom: 30px;
}
.time-container {
background: rgba(0, 0, 0, 0.25);
padding: 25px;
border-radius: 15px;
margin: 25px 0;
border: 2px solid rgba(255, 255, 255, 0.1);
}
.time-display {
font-size: 56px;
font-weight: 700;
margin: 10px 0;
font-family: 'Courier New', monospace;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
letter-spacing: 2px;
}
.date-display {
font-size: 28px;
margin: 15px 0;
opacity: 0.95;
}
.status {
font-size: 18px;
padding: 12px;
border-radius: 10px;
margin: 20px 0;
background: rgba(255, 255, 255, 0.1);
}
.btn-group {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 15px;
margin: 25px 0;
}
button {
background: linear-gradient(to right, #00b09b, #96c93d);
color: white;
border: none;
padding: 16px 32px;
font-size: 18px;
border-radius: 50px;
cursor: pointer;
transition: all 0.3s ease;
font-weight: 600;
min-width: 220px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
button:hover {
transform: translateY(-3px);
box-shadow: 0 7px 20px rgba(0, 0, 0, 0.3);
}
button:active {
transform: translateY(-1px);
}
button.sync {
background: linear-gradient(to right, #ff416c, #ff4b2b);
}
.info-box {
background: rgba(0, 0, 0, 0.2);
padding: 15px;
border-radius: 10px;
margin-top: 25px;
font-size: 14px;
opacity: 0.8;
}
.info-box p {
margin: 8px 0;
}
</style>
</head>
<body>
<div class="container">
<h1>🕐 ESP32 Умные Часы</h1>
<div class="subtitle">Автономные часы с сохранением времени</div>
<div class="time-container">
<div class="date-display" id="date">-- --- ----</div>
<div class="time-display" id="time">--:--:--</div>
<div class="status" id="status">Статус: Загрузка...</div>
</div>
<div class="btn-group">
<button class="sync" onclick="syncPhoneTime()">
🔄 Синхронизировать время
</button>
<button onclick="updateDisplay()">
📡 Обновить время
</button>
</div>
<div class="info-box">
<p><strong>Информация:</strong></p>
<p>📶 WiFi: ESP32_TimeServer</p>
<p>🔑 Пароль: 12345678</p>
<p>🌐 IP: 192.168.4.1</p>
<p>💾 Автосохранение: каждую минуту</p>
<p>⚡ Восстановление: после перезагрузки</p>
</div>
</div>
<script>
function formatTime(date) {
let hours = date.getHours().toString().padStart(2, '0');
let minutes = date.getMinutes().toString().padStart(2, '0');
let seconds = date.getSeconds().toString().padStart(2, '0');
return `${hours}:${minutes}:${seconds}`;
}
function formatDate(date) {
const days = ['Воскресенье', 'Понедельник', 'Вторник', 'Среда',
'Четверг', 'Пятница', 'Суббота'];
const months = ['января', 'февраля', 'марта', 'апреля', 'мая', 'июня',
'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря'];
const dayName = days[date.getDay()];
const day = date.getDate();
const month = months[date.getMonth()];
const year = date.getFullYear();
return `${dayName}, ${day} ${month} ${year} года`;
}
function syncPhoneTime() {
document.getElementById('status').textContent = 'Статус: Синхронизация...';
document.getElementById('status').style.background = 'rgba(255, 215, 0, 0.3)';
const now = new Date();
const timestamp = Math.floor(now.getTime() / 1000);
const timeStr = now.toISOString();
fetch('/settime?ts=' + timestamp + '&str=' + encodeURIComponent(timeStr))
.then(response => {
if (!response.ok) throw new Error('Ошибка сети');
return response.text();
})
.then(data => {
document.getElementById('status').textContent = 'Статус: ' + data;
document.getElementById('status').style.background = 'rgba(0, 255, 0, 0.2)';
updateDisplay();
setTimeout(() => {
document.getElementById('status').style.background = 'rgba(255, 255, 255, 0.1)';
}, 3000);
})
.catch(error => {
document.getElementById('status').textContent = 'Статус: Ошибка синхронизации';
document.getElementById('status').style.background = 'rgba(255, 0, 0, 0.2)';
console.error('Ошибка:', error);
});
}
function updateDisplay() {
fetch('/gettime')
.then(response => {
if (!response.ok) throw new Error('Ошибка сети');
return response.json();
})
.then(data => {
if (data.timestamp > 0) {
const date = new Date(data.timestamp * 1000);
document.getElementById('time').textContent = formatTime(date);
document.getElementById('date').textContent = formatDate(date);
if (data.initialized) {
document.getElementById('status').textContent = 'Статус: Время установлено';
document.getElementById('status').style.background = 'rgba(0, 255, 0, 0.1)';
}
} else {
document.getElementById('time').textContent = "00:00:00";
document.getElementById('date').textContent = "Время не установлено";
document.getElementById('status').textContent = 'Статус: Ожидание синхронизации';
document.getElementById('status').style.background = 'rgba(255, 165, 0, 0.2)';
}
})
.catch(error => {
console.error('Ошибка получения времени:', error);
document.getElementById('status').textContent = 'Статус: Ошибка связи';
document.getElementById('status').style.background = 'rgba(255, 0, 0, 0.2)';
});
}
setInterval(updateDisplay, 1000);
document.addEventListener('DOMContentLoaded', function() {
updateDisplay();
if (!localStorage.getItem('instructionShown')) {
setTimeout(() => {
alert('Для установки времени нажмите кнопку "Синхронизировать время". Время автоматически сохраняется и восстанавливается после перезагрузки.');
localStorage.setItem('instructionShown', 'true');
}, 1000);
}
});
setInterval(updateDisplay, 30000);
</script>
</body>
</html>
)rawliteral";
return html;
}
void handleRoot() {
server.send(200, "text/html", getHTMLPage());
}
void handleGetTime() {
unsigned long currentTime = getCurrentUnixTime();
String json = "{\"timestamp\":" + String(currentTime) +
",\"initialized\":" + String(timeInitialized ? "true" : "false") + "}";
server.send(200, "application/json", json);
}
void handleSetTime() {
if (server.hasArg("ts")) {
unsigned long newTimestamp = server.arg("ts").toInt();
if (newTimestamp > 1609459200 && newTimestamp < 1893456000) {
savedTime.timestamp = newTimestamp;
savedTime.lastSaveMillis = millis();
timeInitialized = true;
saveTimeToEEPROM();
String response = "Время установлено: " + unixTimeToString(newTimestamp);
Serial.println("✓ " + response);
server.send(200, "text/plain", response);
} else {
server.send(400, "text/plain", "Ошибка: неверное значение времени");
}
} else {
server.send(400, "text/plain", "Ошибка: отсутствует параметр ts");
}
}
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("=== ESP32 Умные Часы ===");
Serial.println("Версия 2.0 - С исправлением сохранения времени");
Serial.println("==============================================");
EEPROM.begin(512);
delay(500);
if (loadTimeFromEEPROM()) {
Serial.println("✓ Система времени инициализирована");
} else {
Serial.println("✗ Время не инициализировано. Используйте веб-интерфейс.");
}
Serial.println("📡 Настройка точки доступа...");
WiFi.softAP(ap_ssid, ap_password);
IPAddress IP = WiFi.softAPIP();
Serial.println("✅ Точка доступа запущена:");
Serial.println(" SSID: " + String(ap_ssid));
Serial.println(" Пароль: " + String(ap_password));
Serial.println(" IP: " + IP.toString());
Serial.println(" Канал: " + String(WiFi.channel()));
Serial.println(" Клиентов: " + String(WiFi.softAPgetStationNum()));
server.on("/", handleRoot);
server.on("/gettime", handleGetTime);
server.on("/settime", handleSetTime);
server.begin();
Serial.println("✅ HTTP сервер запущен на порту 80");
Serial.println("👉 Откройте в браузере: http://192.168.4.1");
Serial.println("==============================================");
}
void loop() {
server.handleClient();
static unsigned long lastSaveTime = 0;
if (timeInitialized && (millis() - lastSaveTime >= 60000)) {
lastSaveTime = millis();
saveTimeToEEPROM();
static unsigned long lastPrint = 0;
if (millis() - lastPrint >= 30000) {
lastPrint = millis();
unsigned long currentTime = getCurrentUnixTime();
Serial.println("⏰ Текущее время: " + unixTimeToString(currentTime) +
" (" + String(currentTime) + ")");
Serial.println(" Прошло секунд с сохранения: " + String((millis() - savedTime.lastSaveMillis) / 1000));
}
}
}egin();
Serial.println("✅ HTTP сервер запущен на порту 80");
Serial.println("👉 Откройте в браузере: http://192.168.4.1");
Serial.println("==============================================");
}
void loop() {
server.handleClient();
static unsigned long lastSaveTime = 0;
if (timeInitialized && (millis() - lastSaveTime >= 60000)) {
lastSaveTime = millis();
saveTimeToEEPROM();
static unsigned long lastPrint = 0;
if (millis() - lastPrint >= 30000) {
lastPrint = millis();
unsigned long currentTime = getCurrentUnixTime();
Serial.println("⏰ Текущее время: " + unixTimeToString(currentTime) +
" (" + String(currentTime) + ")");
Serial.println(" Прошло секунд с сохранения: " + String((millis() - savedTime.lastSaveMillis) / 1000));
}
}
}
Ваш вариант имеет право на жизнь, имхо. Только у вас я не вижу выставления временной зоны и что будем делать если смарта нет для настройки, а выставить время надо вот сейчас?
Кстати, если сможете автоматом подтянуть TZ со смарта, будет круто!
Мне тут подсказали на другом форуме, что роутер по DHCP может отдавать и TZ и NTP, но надо патчить idf. Вот тогда будет работать совсем автоматом.
З.Ы. а что у вас будет, если настроенное устройство проведёт в выключенном состоянии более 50 дней, а потом его включат?
¯\_(ツ)_/¯
это вы уже доделаете для всех, если захотите)))
а зачем ? если время просто обновляется и становится такое же как на телефоне….
#include <WiFi.h>
#include <WebServer.h>
#include <EEPROM.h>
#include <time.h>
const char* ap_ssid = "ESP32_TimeServer";
const char* ap_password = "12345678";
WebServer server(80);
struct TimeData {
unsigned long timestamp;
unsigned long lastSaveMillis;
int timezoneOffset; // Смещение в секундах (например, +10800 для GMT+3)
bool dst; // Летнее время
uint32_t crc32;
};
TimeData savedTime;
bool timeInitialized = false;
uint32_t calculateCRC32(const uint8_t *data, size_t length);
void saveTimeToEEPROM();
bool loadTimeFromEEPROM();
unsigned long getCurrentUnixTime();
String unixTimeToString(unsigned long unixTime);
String getHTMLPage();
void handleRoot();
void handleGetTime();
void handleSetTime();
uint32_t calculateCRC32(const uint8_t *data, size_t length) {
uint32_t crc = 0xffffffff;
for (size_t i = 0; i < length; i++) {
crc ^= (uint32_t)data[i] << 24;
for (int j = 0; j < 8; j++) {
if (crc & 0x80000000) {
crc = (crc << 1) ^ 0x04C11DB7;
} else {
crc <<= 1;
}
}
}
return crc;
}
void saveTimeToEEPROM() {
if (!timeInitialized) return;
savedTime.timestamp = getCurrentUnixTime();
savedTime.lastSaveMillis = millis();
savedTime.crc32 = calculateCRC32((uint8_t*)&savedTime, sizeof(savedTime) - sizeof(savedTime.crc32));
EEPROM.put(0, savedTime);
if (EEPROM.commit()) {
Serial.println("✓ Время сохранено в EEPROM: " + String(savedTime.timestamp) +
" (" + unixTimeToString(savedTime.timestamp) + ")");
} else {
Serial.println("✗ Ошибка сохранения в EEPROM!");
}
}
bool loadTimeFromEEPROM() {
EEPROM.get(0, savedTime);
uint32_t calculatedCRC = calculateCRC32((uint8_t*)&savedTime, sizeof(savedTime) - sizeof(savedTime.crc32));
bool isValidTime = (savedTime.timestamp > 1609459200) && (savedTime.timestamp < 1893456000);
if (savedTime.crc32 == calculatedCRC && isValidTime) {
timeInitialized = true;
Serial.println("✓ Время загружено из EEPROM: " + String(savedTime.timestamp) +
" (" + unixTimeToString(savedTime.timestamp) + ")");
Serial.println(" Часовой пояс: GMT" + String(savedTime.timezoneOffset >= 0 ? "+" : "") +
String(savedTime.timezoneOffset/3600.0, 1));
Serial.println(" Летнее время: " + String(savedTime.dst ? "Да" : "Нет"));
return true;
}
savedTime.timestamp = 0;
savedTime.lastSaveMillis = 0;
savedTime.timezoneOffset = 10800; // GMT+3 по умолчанию (Москва)
savedTime.dst = false;
savedTime.crc32 = 0;
timeInitialized = false;
Serial.println("✗ EEPROM содержит невалидные данные или пуста");
return false;
}
unsigned long getCurrentUnixTime() {
if (!timeInitialized) return 0;
if (millis() < savedTime.lastSaveMillis) {
return savedTime.timestamp + (millis() / 1000);
} else {
return savedTime.timestamp + ((millis() - savedTime.lastSaveMillis) / 1000);
}
}
String unixTimeToString(unsigned long unixTime) {
if (unixTime == 0 || unixTime < 1609459200) return "Время не установлено";
time_t rawtime = unixTime;
struct tm *timeinfo;
timeinfo = localtime(&rawtime);
// Применяем сохраненное смещение
timeinfo->tm_hour += (savedTime.timezoneOffset / 3600);
timeinfo->tm_min += ((savedTime.timezoneOffset % 3600) / 60);
if (savedTime.dst) {
timeinfo->tm_hour += 1; // +1 час для летнего времени
}
mktime(timeinfo); // Нормализуем время
char buffer[30];
strftime(buffer, sizeof(buffer), "%d.%m.%Y %H:%M:%S", timeinfo);
String result = String(buffer);
// Добавляем информацию о часовом поясе
result += " (GMT";
result += savedTime.timezoneOffset >= 0 ? "+" : "";
result += String(savedTime.timezoneOffset/3600.0, 1);
if (savedTime.dst) result += ", летнее время";
result += ")";
return result;
}
String getHTMLPage() {
String html = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ESP32 Smart Clock</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
text-align: center;
padding: 20px;
background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%);
color: white;
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.container {
background: rgba(255, 255, 255, 0.15);
padding: 30px;
border-radius: 20px;
backdrop-filter: blur(15px);
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.2);
max-width: 500px;
width: 90%;
border: 1px solid rgba(255, 255, 255, 0.2);
}
h1 {
font-size: 32px;
margin-bottom: 10px;
color: #fff;
text-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
}
.subtitle {
font-size: 16px;
opacity: 0.9;
margin-bottom: 30px;
}
.time-container {
background: rgba(0, 0, 0, 0.25);
padding: 25px;
border-radius: 15px;
margin: 25px 0;
border: 2px solid rgba(255, 255, 255, 0.1);
}
.time-display {
font-size: 56px;
font-weight: 700;
margin: 10px 0;
font-family: 'Courier New', monospace;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
letter-spacing: 2px;
}
.date-display {
font-size: 28px;
margin: 15px 0;
opacity: 0.95;
}
.timezone-info {
font-size: 16px;
margin: 10px 0;
opacity: 0.9;
}
.status {
font-size: 18px;
padding: 12px;
border-radius: 10px;
margin: 20px 0;
background: rgba(255, 255, 255, 0.1);
}
.btn-group {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 15px;
margin: 25px 0;
}
button {
background: linear-gradient(to right, #00b09b, #96c93d);
color: white;
border: none;
padding: 16px 32px;
font-size: 18px;
border-radius: 50px;
cursor: pointer;
transition: all 0.3s ease;
font-weight: 600;
min-width: 220px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
button:hover {
transform: translateY(-3px);
box-shadow: 0 7px 20px rgba(0, 0, 0, 0.3);
}
button:active {
transform: translateY(-1px);
}
button.sync {
background: linear-gradient(to right, #ff416c, #ff4b2b);
}
.info-box {
background: rgba(0, 0, 0, 0.2);
padding: 15px;
border-radius: 10px;
margin-top: 25px;
font-size: 14px;
opacity: 0.8;
}
.info-box p {
margin: 8px 0;
}
</style>
</head>
<body>
<div class="container">
<h1>🕐 ESP32 Умные Часы</h1>
<div class="subtitle">Часы с автоопределением часового пояса</div>
<div class="time-container">
<div class="date-display" id="date">-- --- ----</div>
<div class="time-display" id="time">--:--:--</div>
<div class="timezone-info" id="timezone">Часовой пояс: не установлен</div>
<div class="status" id="status">Статус: Загрузка...</div>
</div>
<div class="btn-group">
<button class="sync" onclick="syncPhoneTime()">
🔄 Синхронизировать время
</button>
<button onclick="updateDisplay()">
📡 Обновить время
</button>
</div>
<div class="info-box">
<p><strong>Информация:</strong></p>
<p>📶 WiFi: ESP32_TimeServer</p>
<p>🔑 Пароль: 12345678</p>
<p>🌐 IP: 192.168.4.1</p>
<p>🗺️ Автопоиск: часовой пояс и летнее время</p>
<p>💾 Автосохранение: каждые 5 минут</p>
</div>
</div>
<script>
// Получение смещения часового пояса в минутах
function getTimezoneOffset() {
return new Date().getTimezoneOffset();
}
// Проверка летнего времени (упрощенная)
function isDST() {
const today = new Date();
const jan = new Date(today.getFullYear(), 0, 1);
const jul = new Date(today.getFullYear(), 6, 1);
const stdTimezoneOffset = Math.max(
jan.getTimezoneOffset(),
jul.getTimezoneOffset()
);
return today.getTimezoneOffset() < stdTimezoneOffset;
}
function formatTime(date) {
let hours = date.getHours().toString().padStart(2, '0');
let minutes = date.getMinutes().toString().padStart(2, '0');
let seconds = date.getSeconds().toString().padStart(2, '0');
return `${hours}:${minutes}:${seconds}`;
}
function formatDate(date) {
const days = ['Воскресенье', 'Понедельник', 'Вторник', 'Среда',
'Четверг', 'Пятница', 'Суббота'];
const months = ['января', 'февраля', 'марта', 'апреля', 'мая', 'июня',
'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря'];
const dayName = days[date.getDay()];
const day = date.getDate();
const month = months[date.getMonth()];
const year = date.getFullYear();
return `${dayName}, ${day} ${month} ${year} года`;
}
function formatTimezone(offsetMinutes, dst) {
const offsetHours = Math.abs(Math.floor(offsetMinutes / 60));
const offsetMins = Math.abs(offsetMinutes % 60);
const sign = offsetMinutes <= 0 ? '+' : '-';
let tzString = `GMT${sign}${offsetHours}`;
if (offsetMins > 0) tzString += `:${offsetMins.toString().padStart(2, '0')}`;
if (dst) tzString += ' (летнее время)';
return tzString;
}
function syncPhoneTime() {
document.getElementById('status').textContent = 'Статус: Синхронизация...';
document.getElementById('status').style.background = 'rgba(255, 215, 0, 0.3)';
const now = new Date();
const timestamp = Math.floor(now.getTime() / 1000);
const timezoneOffset = -getTimezoneOffset() * 60; // Конвертируем в секунды
const dst = isDST();
const timeStr = now.toISOString();
const url = `/settime?ts=${timestamp}&tz=${timezoneOffset}&dst=${dst ? 1 : 0}&str=${encodeURIComponent(timeStr)}`;
fetch(url)
.then(response => {
if (!response.ok) throw new Error('Ошибка сети');
return response.text();
})
.then(data => {
document.getElementById('status').textContent = 'Статус: ' + data;
document.getElementById('status').style.background = 'rgba(0, 255, 0, 0.2)';
updateDisplay();
setTimeout(() => {
document.getElementById('status').style.background = 'rgba(255, 255, 255, 0.1)';
}, 3000);
})
.catch(error => {
document.getElementById('status').textContent = 'Статус: Ошибка синхронизации';
document.getElementById('status').style.background = 'rgba(255, 0, 0, 0.2)';
console.error('Ошибка:', error);
});
}
function updateDisplay() {
fetch('/gettime')
.then(response => {
if (!response.ok) throw new Error('Ошибка сети');
return response.json();
})
.then(data => {
if (data.timestamp > 0) {
const date = new Date(data.timestamp * 1000);
document.getElementById('time').textContent = formatTime(date);
document.getElementById('date').textContent = formatDate(date);
document.getElementById('timezone').textContent =
`Часовой пояс: ${data.timezone || 'GMT+3'}`;
if (data.initialized) {
document.getElementById('status').textContent = 'Статус: Время установлено';
document.getElementById('status').style.background = 'rgba(0, 255, 0, 0.1)';
}
} else {
document.getElementById('time').textContent = "00:00:00";
document.getElementById('date').textContent = "Время не установлено";
document.getElementById('timezone').textContent = "Часовой пояс: не установлен";
document.getElementById('status').textContent = 'Статус: Ожидание синхронизации';
document.getElementById('status').style.background = 'rgba(255, 165, 0, 0.2)';
}
})
.catch(error => {
console.error('Ошибка получения времени:', error);
document.getElementById('status').textContent = 'Статус: Ошибка связи';
document.getElementById('status').style.background = 'rgba(255, 0, 0, 0.2)';
});
}
setInterval(updateDisplay, 1000);
document.addEventListener('DOMContentLoaded', function() {
updateDisplay();
if (!localStorage.getItem('instructionShown')) {
setTimeout(() => {
alert('Нажмите "Синхронизировать время" для автоматической настройки:\n' +
'• Текущего времени\n' +
'• Часового пояса\n' +
'• Летнего времени\n\n' +
'Время сохраняется при отключении питания!');
localStorage.setItem('instructionShown', 'true');
}, 1000);
}
});
</script>
</body>
</html>
)rawliteral";
return html;
}
void handleRoot() {
server.send(200, "text/html", getHTMLPage());
}
void handleGetTime() {
unsigned long currentTime = getCurrentUnixTime();
String timezoneStr = "GMT";
timezoneStr += savedTime.timezoneOffset >= 0 ? "+" : "";
timezoneStr += String(savedTime.timezoneOffset/3600.0, 1);
if (savedTime.dst) timezoneStr += " (летнее время)";
String json = "{\"timestamp\":" + String(currentTime) +
",\"initialized\":" + String(timeInitialized ? "true" : "false") +
",\"timezone\":\"" + timezoneStr + "\"}";
server.send(200, "application/json", json);
}
void handleSetTime() {
if (server.hasArg("ts")) {
unsigned long newTimestamp = server.arg("ts").toInt();
int timezoneOffset = server.hasArg("tz") ? server.arg("tz").toInt() : 10800;
bool dst = server.hasArg("dst") ? (server.arg("dst").toInt() == 1) : false;
if (newTimestamp > 1609459200 && newTimestamp < 1893456000) {
savedTime.timestamp = newTimestamp;
savedTime.lastSaveMillis = millis();
savedTime.timezoneOffset = timezoneOffset;
savedTime.dst = dst;
timeInitialized = true;
saveTimeToEEPROM();
String response = "Время установлено: " + unixTimeToString(newTimestamp);
Serial.println("✓ " + response);
Serial.println(" Часовой пояс: GMT" + String(timezoneOffset >= 0 ? "+" : "") +
String(timezoneOffset/3600.0, 1));
Serial.println(" Летнее время: " + String(dst ? "Да" : "Нет"));
server.send(200, "text/plain", response);
} else {
server.send(400, "text/plain", "Ошибка: неверное значение времени");
}
} else {
server.send(400, "text/plain", "Ошибка: отсутствует параметр ts");
}
}
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("=== ESP32 Умные Часы ===");
Serial.println("Версия 3.0 - С автоопределением часового пояса");
Serial.println("==============================================");
EEPROM.begin(512);
delay(500);
if (loadTimeFromEEPROM()) {
Serial.println("✓ Система времени инициализирована");
} else {
Serial.println("✗ Время не инициализировано. Используйте веб-интерфейс.");
}
Serial.println("📡 Настройка точки доступа...");
WiFi.softAP(ap_ssid, ap_password);
IPAddress IP = WiFi.softAPIP();
Serial.println("✅ Точка доступа запущена:");
Serial.println(" SSID: " + String(ap_ssid));
Serial.println(" Пароль: " + String(ap_password));
Serial.println(" IP: " + IP.toString());
Serial.println(" Канал: " + String(WiFi.channel()));
Serial.println(" Клиентов: " + String(WiFi.softAPgetStationNum()));
server.on("/", handleRoot);
server.on("/gettime", handleGetTime);
server.on("/settime", handleSetTime);
server.begin();
Serial.println("✅ HTTP сервер запущен на порту 80");
Serial.println("👉 Откройте в браузере: http://192.168.4.1");
Serial.println("==============================================");
}
void loop() {
server.handleClient();
static unsigned long lastSaveTime = 0;
if (timeInitialized && (millis() - lastSaveTime >= 300000)) { // Каждые 5 минут
lastSaveTime = millis();
saveTimeToEEPROM();
static unsigned long lastPrint = 0;
if (millis() - lastPrint >= 30000) {
lastPrint = millis();
unsigned long currentTime = getCurrentUnixTime();
Serial.println("⏰ Текущее время: " + unixTimeToString(currentTime));
Serial.println(" Секунд с синхронизации: " + String((millis() - savedTime.lastSaveMillis) / 1000));
}
}
}
вот если я вас правильно понял… даль сами)))
p.s. сервер часов находится на самой плате, она как роутер
ну это в России оно постоянное сейчас, а в других странах есть перевод на зимнее и летнее время. У меня оно станет актуальным в момент включения устройства, а у Вас?
так я и спрашиваю, какое время загрузится? Смарта нет, никто не контролирует в этот момент. Подсказка: millis() пошёл на следующий круг.
для других стран есть библиотека <NTPClient.h> с работой через интернет, и автоматической корректировкой, а это офлайн версия, с настройкой такого же времени при соединении с телефоном, последняя версия кода вроде учитывает все… и если что сможете доделать, на авторство не претендую, возьмите за основу, и улучшайте проект
она не выставляет TZ автоматом, там ручками надо в коде или в настройках (если предусмотреть). Я этот вопрос и хотел решить.
вот мне сильно кажется, что если устройство “поспит” 49+ дней и потом включить без смарта, то часы будут очень сильно отставать (на эти 49 дней).
Ну и дырку во флеше трёт.
Я не говорю что моя версия идеальная, там свои “тараканы”. Особенно в наше время ![]()
если мы модернизируем вашу, тогда надо добавить больше серверов
“pool.ntp.org”,
“time.nist.gov”,
“ru.pool.ntp.org”,
“ntp1.stratum2.ru”
до обновления через телефон, естественно, часы не выходят в интернет…
вы вообще загружали мой код ?)))
NTP-сервера задаются у меня при вызове. Как автор напишет, такие и будут. У меня просто обёртка для configTime, которая передает ему до трёх серверов. У меня ничего не хранится в библиотеки.
я его внимательно посмотрел. И если не ошибся, нашел серьезный прокол с миллис. Нельзя на него полагаться. Он одинаков каждые 49+ дней. Так что при следующем включении мы можем иметь отставание на 49, 98 и т.д. дней, мне так кажется.
Проверять сейчас не буду, но на днях посмотрю.
#include <WiFi.h>
#include <WebServer.h>
#include <NTPClient.h>
#include <WiFiUdp.h>
#include <EEPROM.h>
#include <time.h>
WebServer server(80);
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP);
// Структура для сохранения всех настроек
struct ConfigData {
char wifiSSID[32];
char wifiPassword[32];
unsigned long timestamp;
unsigned long lastSaveMillis;
int timezoneOffset;
bool dst;
bool useNTP;
bool configured;
uint32_t crc32;
};
ConfigData config;
bool timeInitialized = false;
bool inAPMode = true;
uint32_t calculateCRC32(const uint8_t *data, size_t length) {
uint32_t crc = 0xffffffff;
for (size_t i = 0; i < length; i++) {
crc ^= (uint32_t)data[i] << 24;
for (int j = 0; j < 8; j++) {
if (crc & 0x80000000) {
crc = (crc << 1) ^ 0x04C11DB7;
} else {
crc <<= 1;
}
}
}
return crc;
}
void saveConfig() {
config.crc32 = calculateCRC32((uint8_t*)&config, sizeof(config) - sizeof(config.crc32));
EEPROM.put(0, config);
EEPROM.commit();
Serial.println("✓ Конфигурация сохранена");
}
bool loadConfig() {
EEPROM.get(0, config);
uint32_t calculatedCRC = calculateCRC32((uint8_t*)&config, sizeof(config) - sizeof(config.crc32));
bool isValid = (config.crc32 == calculatedCRC) && config.configured;
if (isValid) {
Serial.println("✓ Конфигурация загружена");
Serial.println(" WiFi: " + String(config.wifiSSID));
Serial.println(" NTP: " + String(config.useNTP ? "Включен" : "Выключен"));
if (config.timestamp > 1609459200) {
timeInitialized = true;
Serial.println(" Время: " + String(config.timestamp));
}
return true;
}
// Значения по умолчанию
strcpy(config.wifiSSID, "");
strcpy(config.wifiPassword, "");
config.timestamp = 0;
config.lastSaveMillis = 0;
config.timezoneOffset = 10800;
config.dst = false;
config.useNTP = true;
config.configured = false;
config.crc32 = 0;
timeInitialized = false;
Serial.println("✗ Конфигурация не найдена");
return false;
}
unsigned long getCurrentUnixTime() {
if (!timeInitialized) return 0;
if (millis() < config.lastSaveMillis) {
return config.timestamp + (millis() / 1000);
} else {
return config.timestamp + ((millis() - config.lastSaveMillis) / 1000);
}
}
void saveCurrentTime() {
config.timestamp = getCurrentUnixTime();
config.lastSaveMillis = millis();
saveConfig();
}
String unixTimeToString(unsigned long unixTime) {
if (unixTime == 0) return "Время не установлено";
time_t rawtime = unixTime;
struct tm *timeinfo;
timeinfo = localtime(&rawtime);
timeinfo->tm_hour += (config.timezoneOffset / 3600);
timeinfo->tm_min += ((config.timezoneOffset % 3600) / 60);
if (config.dst) timeinfo->tm_hour += 1;
mktime(timeinfo);
char buffer[30];
strftime(buffer, sizeof(buffer), "%d.%m.%Y %H:%M:%S", timeinfo);
return String(buffer);
}
void syncWithNTP() {
static unsigned long lastSync = 0;
if (config.useNTP && WiFi.status() == WL_CONNECTED && !inAPMode) {
if (millis() - lastSync > 300000) { // Каждые 5 минут
if (timeClient.update()) {
lastSync = millis();
config.timestamp = timeClient.getEpochTime();
config.lastSaveMillis = millis();
timeInitialized = true;
saveConfig();
Serial.println("✅ Синхронизировано с NTP: " + unixTimeToString(config.timestamp));
} else {
Serial.println("❌ Ошибка синхронизации NTP");
}
}
}
}
String getMainPage() {
String html = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Часы ESP32</title>
<style>
body {font-family: Arial; padding: 20px; background: #f0f0f0;}
.container {max-width: 500px; margin: 0 auto; background: white; padding: 30px; border-radius: 10px; box-shadow: 0 0 20px rgba(0,0,0,0.1);}
h1 {text-align: center; color: #333;}
.time {font-size: 48px; text-align: center; font-family: monospace; margin: 20px 0; color: #2c3e50;}
.date {text-align: center; font-size: 20px; color: #7f8c8d; margin-bottom: 30px;}
.status {background: #ecf0f1; padding: 15px; border-radius: 5px; margin: 15px 0; text-align: center;}
.btn {display: block; width: 100%; padding: 15px; margin: 10px 0; border: none; border-radius: 5px; font-size: 16px; cursor: pointer;}
.primary {background: #3498db; color: white;}
.success {background: #2ecc71; color: white;}
.warning {background: #f39c12; color: white;}
.danger {background: #e74c3c; color: white;}
.info {background: #95a5a6; color: white;}
.tab {overflow: hidden; border: 1px solid #ccc; background: #f1f1f1;}
.tab button {float: left; border: none; outline: none; cursor: pointer; padding: 14px 16px; font-size: 17px;}
.tab button.active {background: #3498db; color: white;}
.tabcontent {display: none; padding: 20px 0;}
.form-group {margin: 15px 0;}
.form-group label {display: block; margin-bottom: 5px; font-weight: bold;}
.form-group input, .form-group select {width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 16px;}
</style>
</head>
<body>
<div class="container">
<h1>⏰ Часы ESP32</h1>
<div class="time" id="currentTime">--:--:--</div>
<div class="date" id="currentDate">-- --- ----</div>
<div class="status" id="status">Загрузка...</div>
<div class="tab">
<button class="tablinks active" onclick="openTab(event, 'TimeTab')">Время</button>
<button class="tablinks" onclick="openTab(event, 'WiFiTab')">WiFi</button>
<button class="tablinks" onclick="openTab(event, 'SettingsTab')">Настройки</button>
</div>
<div id="TimeTab" class="tabcontent" style="display: block;">
<button class="btn success" onclick="syncWithPhone()">Синхронизировать с телефоном</button>
<button class="btn warning" onclick="openManualTime()">Установить вручную</button>
<button class="btn primary" onclick="syncWithNTP()">Синхронизировать через NTP</button>
<div id="manualTimeSection" style="display: none; margin-top: 20px; padding: 20px; background: #f9f9f9; border-radius: 5px;">
<div class="form-group">
<label>Дата:</label>
<input type="date" id="manualDate" value="2024-01-01">
</div>
<div class="form-group">
<label>Время:</label>
<input type="time" id="manualTime" value="12:00">
</div>
<div class="form-group">
<label>Часовой пояс:</label>
<select id="manualTimezone">
<option value="10800">Москва (GMT+3)</option>
<option value="0">Лондон (GMT+0)</option>
<option value="3600">Берлин (GMT+1)</option>
<option value="-18000">Нью-Йорк (GMT-5)</option>
</select>
</div>
<button class="btn success" onclick="setManualTime()">Установить</button>
<button class="btn danger" onclick="closeManualTime()">Отмена</button>
</div>
</div>
<div id="WiFiTab" class="tabcontent">
<div class="form-group">
<label>WiFi SSID:</label>
<input type="text" id="wifiSSID" placeholder="Имя сети WiFi">
</div>
<div class="form-group">
<label>Пароль WiFi:</label>
<input type="password" id="wifiPassword" placeholder="Пароль">
</div>
<button class="btn primary" onclick="saveWiFi()">Сохранить WiFi</button>
<button class="btn info" onclick="scanWiFi()">Сканировать сети</button>
<div id="wifiList" style="margin-top: 20px;"></div>
</div>
<div id="SettingsTab" class="tabcontent">
<div class="form-group">
<label>Режим синхронизации:</label>
<select id="syncMode" onchange="saveSyncMode()">
<option value="ntp">Автосинхронизация NTP</option>
<option value="manual">Только ручная синхронизация</option>
</select>
</div>
<div class="form-group">
<label>Часовой пояс:</label>
<select id="timezone" onchange="saveTimezone()">
<option value="-43200">GMT-12</option>
<option value="-39600">GMT-11</option>
<option value="-36000">GMT-10</option>
<option value="-32400">GMT-9</option>
<option value="-28800">GMT-8</option>
<option value="-25200">GMT-7</option>
<option value="-21600">GMT-6</option>
<option value="-18000">GMT-5</option>
<option value="-14400">GMT-4</option>
<option value="-10800">GMT-3</option>
<option value="-7200">GMT-2</option>
<option value="-3600">GMT-1</option>
<option value="0">GMT+0</option>
<option value="3600">GMT+1</option>
<option value="7200">GMT+2</option>
<option value="10800" selected>GMT+3 (Москва)</option>
<option value="14400">GMT+4</option>
<option value="18000">GMT+5</option>
<option value="21600">GMT+6</option>
<option value="25200">GMT+7</option>
<option value="28800">GMT+8</option>
<option value="32400">GMT+9</option>
<option value="36000">GMT+10</option>
<option value="39600">GMT+11</option>
<option value="43200">GMT+12</option>
</select>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="dstCheckbox" onchange="saveDST()">
Летнее время
</label>
</div>
<button class="btn danger" onclick="resetConfig()">Сбросить настройки</button>
</div>
</div>
<script>
function openTab(evt, tabName) {
var i, tabcontent, tablinks;
tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
tablinks = document.getElementsByClassName("tablinks");
for (i = 0; i < tablinks.length; i++) {
tablinks[i].className = tablinks[i].className.replace(" active", "");
}
document.getElementById(tabName).style.display = "block";
evt.currentTarget.className += " active";
}
function updateTimeDisplay() {
fetch('/gettime')
.then(r => r.json())
.then(data => {
if (data.timestamp > 0) {
const date = new Date(data.timestamp * 1000);
document.getElementById('currentTime').textContent =
date.getHours().toString().padStart(2, '0') + ':' +
date.getMinutes().toString().padStart(2, '0') + ':' +
date.getSeconds().toString().padStart(2, '0');
document.getElementById('currentDate').textContent =
date.toLocaleDateString('ru-RU', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
});
document.getElementById('status').textContent =
'Режим: ' + data.syncMode + ' | TZ: ' + data.timezone;
}
});
}
function syncWithPhone() {
document.getElementById('status').textContent = 'Синхронизация...';
const now = new Date();
const timestamp = Math.floor(now.getTime() / 1000);
const timezoneOffset = -new Date().getTimezoneOffset() * 60;
const dst = new Date().getTimezoneOffset() <
Math.max(new Date(now.getFullYear(), 0, 1).getTimezoneOffset(),
new Date(now.getFullYear(), 6, 1).getTimezoneOffset());
fetch(`/settime?ts=${timestamp}&tz=${timezoneOffset}&dst=${dst ? 1 : 0}`)
.then(r => r.text())
.then(data => {
document.getElementById('status').textContent = data;
updateTimeDisplay();
});
}
function openManualTime() {
document.getElementById('manualTimeSection').style.display = 'block';
}
function closeManualTime() {
document.getElementById('manualTimeSection').style.display = 'none';
}
function setManualTime() {
const date = document.getElementById('manualDate').value;
const time = document.getElementById('manualTime').value;
const timezone = document.getElementById('manualTimezone').value;
const dateTime = new Date(date + 'T' + time);
const timestamp = Math.floor(dateTime.getTime() / 1000);
document.getElementById('status').textContent = 'Установка...';
fetch(`/settime?ts=${timestamp}&tz=${timezone}&dst=0`)
.then(r => r.text())
.then(data => {
document.getElementById('status').textContent = data;
document.getElementById('manualTimeSection').style.display = 'none';
updateTimeDisplay();
});
}
function syncWithNTP() {
document.getElementById('status').textContent = 'NTP синхронизация...';
fetch('/syncntp')
.then(r => r.text())
.then(data => {
document.getElementById('status').textContent = data;
updateTimeDisplay();
});
}
function saveWiFi() {
const ssid = document.getElementById('wifiSSID').value;
const password = document.getElementById('wifiPassword').value;
document.getElementById('status').textContent = 'Сохранение WiFi...';
fetch('/savewifi?ssid=' + encodeURIComponent(ssid) + '&pass=' + encodeURIComponent(password))
.then(r => r.text())
.then(data => {
document.getElementById('status').textContent = data;
});
}
function scanWiFi() {
document.getElementById('wifiList').innerHTML = '<p>Сканирование...</p>';
fetch('/scanwifi')
.then(r => r.text())
.then(data => {
document.getElementById('wifiList').innerHTML = data;
});
}
function saveSyncMode() {
const mode = document.getElementById('syncMode').value;
fetch('/savesyncmode?mode=' + mode)
.then(r => r.text())
.then(data => console.log(data));
}
function saveTimezone() {
const tz = document.getElementById('timezone').value;
fetch('/savetimezone?tz=' + tz)
.then(r => r.text())
.then(data => console.log(data));
}
function saveDST() {
const dst = document.getElementById('dstCheckbox').checked;
fetch('/savedst?dst=' + (dst ? 1 : 0))
.then(r => r.text())
.then(data => console.log(data));
}
function resetConfig() {
if (confirm('Сбросить все настройки?')) {
fetch('/reset')
.then(r => r.text())
.then(data => {
document.getElementById('status').textContent = data;
location.reload();
});
}
}
function loadSettings() {
fetch('/getconfig')
.then(r => r.json())
.then(data => {
if (data.wifiSSID) document.getElementById('wifiSSID').value = data.wifiSSID;
if (data.syncMode) document.getElementById('syncMode').value = data.syncMode;
if (data.timezoneOffset) document.getElementById('timezone').value = data.timezoneOffset;
if (data.dst) document.getElementById('dstCheckbox').checked = data.dst;
});
}
// Обновление времени каждую секунду
setInterval(updateTimeDisplay, 1000);
// Загрузка при старте
document.addEventListener('DOMContentLoaded', function() {
updateTimeDisplay();
loadSettings();
});
</script>
</body>
</html>
)rawliteral";
return html;
}
void handleRoot() {
server.send(200, "text/html", getMainPage());
}
void handleGetTime() {
unsigned long currentTime = getCurrentUnixTime();
String timezoneStr = "GMT";
timezoneStr += config.timezoneOffset >= 0 ? "+" : "";
timezoneStr += String(config.timezoneOffset/3600.0, 1);
if (config.dst) timezoneStr += " (летнее)";
String syncMode = config.useNTP ? "NTP" : "Ручная";
String json = "{\"timestamp\":" + String(currentTime) +
",\"timezone\":\"" + timezoneStr + "\"" +
",\"syncMode\":\"" + syncMode + "\"}";
server.send(200, "application/json", json);
}
void handleSetTime() {
if (server.hasArg("ts")) {
config.timestamp = server.arg("ts").toInt();
config.lastSaveMillis = millis();
config.timezoneOffset = server.hasArg("tz") ? server.arg("tz").toInt() : 10800;
config.dst = server.hasArg("dst") ? (server.arg("dst").toInt() == 1) : false;
timeInitialized = true;
saveConfig();
String response = "Время установлено: " + unixTimeToString(config.timestamp);
server.send(200, "text/plain", response);
Serial.println("✓ " + response);
} else {
server.send(400, "text/plain", "Ошибка: нет параметра ts");
}
}
void handleSaveWiFi() {
if (server.hasArg("ssid") && server.hasArg("pass")) {
String ssid = server.arg("ssid");
String pass = server.arg("pass");
ssid.toCharArray(config.wifiSSID, 32);
pass.toCharArray(config.wifiPassword, 32);
config.configured = true;
saveConfig();
server.send(200, "text/plain", "WiFi сохранен. Перезагрузите устройство.");
Serial.println("✓ WiFi сохранен: " + ssid);
} else {
server.send(400, "text/plain", "Ошибка: нет SSID или пароля");
}
}
void handleScanWiFi() {
String html = "<h3>Доступные сети:</h3><ul>";
int n = WiFi.scanNetworks();
if (n == 0) {
html += "<li>Сети не найдены</li>";
} else {
for (int i = 0; i < n; ++i) {
html += "<li><strong>" + WiFi.SSID(i) + "</strong> (" +
String(WiFi.RSSI(i)) + " dBm)" +
(WiFi.encryptionType(i) == WIFI_AUTH_OPEN ? " 🔓" : " 🔒") +
" <button onclick=\"selectWiFi('" + WiFi.SSID(i) + "')\">Выбрать</button></li>";
}
}
html += "</ul><script>function selectWiFi(ssid){document.getElementById('wifiSSID').value=ssid;}</script>";
server.send(200, "text/html", html);
}
void handleSyncNTP() {
if (WiFi.status() == WL_CONNECTED) {
if (timeClient.update()) {
config.timestamp = timeClient.getEpochTime();
config.lastSaveMillis = millis();
timeInitialized = true;
saveConfig();
server.send(200, "text/plain", "NTP синхронизация успешна");
} else {
server.send(200, "text/plain", "Ошибка NTP синхронизации");
}
} else {
server.send(200, "text/plain", "Нет подключения к WiFi");
}
}
void handleSaveSyncMode() {
if (server.hasArg("mode")) {
config.useNTP = (server.arg("mode") == "ntp");
saveConfig();
server.send(200, "text/plain", "Режим синхронизации сохранен");
}
}
void handleSaveTimezone() {
if (server.hasArg("tz")) {
config.timezoneOffset = server.arg("tz").toInt();
saveConfig();
server.send(200, "text/plain", "Часовой пояс сохранен");
}
}
void handleSaveDST() {
if (server.hasArg("dst")) {
config.dst = (server.arg("dst").toInt() == 1);
saveConfig();
server.send(200, "text/plain", "Настройка летнего времени сохранена");
}
}
void handleGetConfig() {
String json = "{\"wifiSSID\":\"" + String(config.wifiSSID) + "\"" +
",\"syncMode\":\"" + String(config.useNTP ? "ntp" : "manual") + "\"" +
",\"timezoneOffset\":" + String(config.timezoneOffset) +
",\"dst\":" + String(config.dst ? "true" : "false") + "}";
server.send(200, "application/json", json);
}
void handleReset() {
EEPROM.write(0, 0xFF);
EEPROM.commit();
server.send(200, "text/plain", "Настройки сброшены. Перезагрузите устройство.");
}
void tryConnectToWiFi() {
if (strlen(config.wifiSSID) > 0) {
Serial.println("📡 Попытка подключения к сохраненному WiFi...");
Serial.println(" SSID: " + String(config.wifiSSID));
WiFi.begin(config.wifiSSID, config.wifiPassword);
for (int i = 0; i < 20; i++) {
if (WiFi.status() == WL_CONNECTED) {
inAPMode = false;
Serial.println("✅ Подключено к WiFi");
Serial.println(" IP: " + WiFi.localIP().toString());
timeClient.begin();
timeClient.setUpdateInterval(300000);
if (config.useNTP) {
if (timeClient.update()) {
config.timestamp = timeClient.getEpochTime();
config.lastSaveMillis = millis();
timeInitialized = true;
saveConfig();
Serial.println("✅ Время получено через NTP");
}
}
return;
}
delay(500);
Serial.print(".");
}
Serial.println("❌ Не удалось подключиться к WiFi");
}
inAPMode = true;
WiFi.softAP("ESP32_Clock", "12345678");
Serial.println("📡 Запущена точка доступа:");
Serial.println(" SSID: ESP32_Clock");
Serial.println(" Пароль: 12345678");
Serial.println(" IP: " + WiFi.softAPIP().toString());
}
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("=== ESP32 Умные Часы ===");
Serial.println("Версия 5.0 - Полная настройка через веб");
Serial.println("========================================");
EEPROM.begin(512);
loadConfig();
tryConnectToWiFi();
server.on("/", handleRoot);
server.on("/gettime", handleGetTime);
server.on("/settime", handleSetTime);
server.on("/savewifi", handleSaveWiFi);
server.on("/scanwifi", handleScanWiFi);
server.on("/syncntp", handleSyncNTP);
server.on("/savesyncmode", handleSaveSyncMode);
server.on("/savetimezone", handleSaveTimezone);
server.on("/savedst", handleSaveDST);
server.on("/getconfig", handleGetConfig);
server.on("/reset", handleReset);
server.begin();
Serial.println("✅ Веб-сервер запущен");
if (inAPMode) {
Serial.println("👉 Откройте: http://192.168.4.1");
} else {
Serial.println("👉 Откройте: http://" + WiFi.localIP().toString());
}
}
void loop() {
server.handleClient();
syncWithNTP();
static unsigned long lastSave = 0;
if (timeInitialized && millis() - lastSave > 300000) {
lastSave = millis();
saveCurrentTime();
Serial.println("💾 Автосохранение времени");
}
}
а если есть реальное желание сделать, вот может это подойдет за основу, не уверен что все работает, но все можно настроить…
и офлайн версия, и через интернет, но ее надо тестировать часа 3, а времени нет)))
переполнение, да наверное есть)))
Не критики ради, а для общего понятия. Сохранять имя сети и пароль во флеш не нужно, есть возможность передать это системе. Тем более пароль в открытом виде - моветон.
GMT - устаревшее название, сейчас используют UTC.
а у вас это работает? Строка то в страничке есть, только у меня она реально функцию не объявляет и не находит по нажатию кнопки Выбрать.
(index):38 Uncaught ReferenceError: openTab is not defined
at HTMLButtonElement.onclick ((index):38:62)
onclick @ (index):38
content.js:1 Uncaught (in promise) The message port closed before a response was received.
(anonymous) @ content.js:1
content.js:1 Uncaught (in promise) The message port closed before a response was received.
(anonymous) @ content.js:1
favicon.ico:1 GET http://192.168.1.58/favicon.ico 404 (Not Found)
(index):1 Uncaught ReferenceError: selectWiFi is not defined
at HTMLButtonElement.onclick ((index):1:1)
onclick @ (index):1
ну и по самому выбору есть вопрос. У меня mesh-сеть и сетей с одним названием несколько находится, кнопка никак не учитывает BSSID станции и, соответственно, никак не может выбрать нужную при нажатии такой кнопки. Это если перенести скрипт в общий блок скриптов, а так его и нет как будто.
Но это мелочи. Самое главное я не пойму зачем вы пилите флэш. Время всё равно сбивается - отключили свет на 1 час и ваши часы отстали на час+время последнего сохранения . Без энергии они не идут и время не могут правильно выставить. Тут без RTC не обойтись никак. А если есть RTC - то зачем пилить флэш?
мельком тестировал, на ардуино иде 1.8.19 на версии ядра esp32 <3 версии а какими вы инструментами пользуетесь не знаю….
вот код под esp8266
с ип адресами у меня проблема, я не смогу наверное затестировать работу кода по автоматическому определению часового пояса, у вас будет работать этот код ?
#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#include <NTPClient.h>
#include <ArduinoJson.h>
const char* ssid = "espff";
const char* password = "12345678";
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "pool.ntp.org");
// Переменные для часового пояса
int timezoneOffset = 0; // в минутах
int totalOffsetSeconds = 0; // в секундах
String timezoneStr = "UTC";
void setup() { Serial.begin(115200); delay(1000);
Serial.println("ESP8266 Time Client with Auto Timezone");
Serial.println("======================================");
// Подключение к Wi-Fi
WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) {
delay(500); Serial.print("."); }
Serial.println("WiFi connected!");
// Инициализация NTP
timeClient.begin();
timeClient.setUpdateInterval(60000);
// Определение часового пояса
determineTimezone();
// Применение настроек
applyTimezone();
// Первое обновление
timeClient.forceUpdate();
Serial.println("System ready!");
Serial.println("======================================");
}
void loop() {
timeClient.update();
static unsigned long lastPrint = 0;
if (millis() - lastPrint >= 1000) {
printTime();
lastPrint = millis();
}
}
void determineTimezone() {
Serial.println("Determining timezone...");
WiFiClient client;
// Используем простой сервис для определения часового пояса
if (client.connect("worldtimeapi.org", 80)) {
client.println("GET /api/ip HTTP/1.1");
client.println("Host: worldtimeapi.org");
client.println("Connection: close");
client.println();
delay(1500);
String response = "";
while (client.available()) {
response += client.readStringUntil('\n');
}
client.stop();
// Парсим JSON
DynamicJsonDocument doc(1024);
DeserializationError error = deserializeJson(doc, response);
if (!error) {
if (doc.containsKey("utc_offset")) {
String offset = doc["utc_offset"].as<String>();
timezoneStr = doc["timezone"].as<String>();
// Парсим смещение "+03:00" или "-05:00"
parseOffset(offset);
Serial.print("Timezone detected: ");
Serial.println(timezoneStr);
Serial.print("UTC offset: ");
Serial.println(offset);
return;
}
}
}
// Если не удалось, используем по умолчанию
Serial.println("Using default timezone: UTC+3 (Moscow)");
timezoneOffset = 180; // UTC+3
timezoneStr = "Europe/Moscow";
}
void parseOffset(String offset) {
// Формат: "+03:00" или "-05:30"
bool negative = offset.charAt(0) == '-';
int startIdx = negative ? 1 : 0;
String hourPart = offset.substring(startIdx, startIdx + 2);
String minutePart = offset.substring(startIdx + 3, startIdx + 5);
int hours = hourPart.toInt();
int minutes = minutePart.toInt();
timezoneOffset = hours * 60 + minutes;
if (negative) {
timezoneOffset = -timezoneOffset;
}
}
void applyTimezone() {
totalOffsetSeconds = timezoneOffset * 60; // в секундах
timeClient.setTimeOffset(totalOffsetSeconds);
Serial.print("Applied offset: ");
Serial.print(timezoneOffset / 60);
Serial.print("h ");
if (timezoneOffset % 60 != 0) {
Serial.print(abs(timezoneOffset % 60));
Serial.print("m");
}
Serial.println();
}
void printTime() {
// Получаем время от NTP клиента (уже с учетом смещения)
String timeStr = timeClient.getFormattedTime();
// Получаем сырое время и добавляем смещение для даты
time_t rawTime = timeClient.getEpochTime();
time_t localTime = rawTime + totalOffsetSeconds;
struct tm *timeinfo = gmtime(&localTime);
// Форматируем дату
char dateStr[20];
sprintf(dateStr, "%02d.%02d.%04d",
timeinfo->tm_mday,
timeinfo->tm_mon + 1,
timeinfo->tm_year + 1900);
// Форматируем время с секундами
char fullTimeStr[20];
sprintf(fullTimeStr, "%02d:%02d:%02d",
timeinfo->tm_hour,
timeinfo->tm_min,
timeinfo->tm_sec);
// Выводим
Serial.print("Date: ");
Serial.print(dateStr);
Serial.print(" | Time: ");
Serial.print(fullTimeStr);
Serial.print(" | TZ: ");
Serial.print(timezoneStr);
Serial.print(" UTC");
if (timezoneOffset >= 0) Serial.print("+");
Serial.print(timezoneOffset / 60);
if (timezoneOffset % 60 != 0) {
Serial.print(":");
Serial.print(abs(timezoneOffset % 60));
}
Serial.println();
}
во флеш пихаю, что бы сделать часы устойчивыми при работе в разных странах))) от сюда время с телефона… + пытаюсь забайтить вас сделать действительно полезный инструмент для всех)))
по идеи, для часов надо делать веб сервер, на веб сервере уже можно сделать выбор часового пояса, и включать галочку о переходе на летнее и зимнее время,(в ручную сохранением настроек) + добавив остальное если нужно… будильник, изменение wifi логина и пароля с сохранением и т.д.
это все что нужно вроде от часов… есть интернет, часы работают, нет интернета … в веб сервере можно еще указать новый адрес для получения времени с сайта… или ждать восстановления его работы)))
нужно ли добавлять модуль часов, или локальные часы с установкой времени не знаю…
честно говоря я тоже не понимаю что вы делаете и зачем))) но варианты что можно сделать вроде накидываю, и примеры что взять за основу для получения действительно полезного инструмента для всех, но может и то как вы делаете лучше…
p.s. у меня главная проблема нехватка времени, я даже сообщения читаю мельком иногда,(за пару секунд, зато быстро!))))
возможно если оно все таки будет, сделаем вдвоем что то… главное понять бы что надо в часах, и как это должно работать…
как вариант что там должно быть и как работать я вроде предложил…
так я то не часы делал. Я просто устанавливал время на esp с локальным часовым поясом. Без участия человека.
По идеи, где бы не вошел в сеть, у тебя точное локальное время без участия человека после подключения к wifi (если без vpn).
А у вас постоянно кто то должен присутствовать в сети и синхронизировать время на интернет часах при сбоях?
Если надо корректировать, то непонятно зачем писать во флеш. Зачем нам знать последние millis и время при этом?
При следующем запуске оно просто подтягивается из флеша, но к реальному отношения не имеет. И millis тут не спасают.
Если нет синхронизации, то время отстаёт на время отключения, а если есть, то оно и не нужно. При сихронизации выставится.
Если уже есть веб-морда, то там вполне можно указать часовой пояс и не париться вообще.
роботы и серверы о себя позаботятся сами, а о людях кто подумает ? вы что их не любите ?
либо человек, либо автоматически через сеть, можно так же добавить автоматическую коррекцию времени раз в час(тоесть есp эта сама и на кнопку будет нажимать, я просто как пример скинул)
если питание пропадет, и надо восcтановить последнее время, модуль часов тоже вариант….
а ваш код для чего ? есть еще обновление прошивки через интернет… там и пояс можно указать в новой, но это так, как вариант….
а вот если для кастомного дрона…. с интернет управлением, может быть ваш код то что нужно… я просто не понял для чего вы делаете, и я никак не осуждаю!))) у кого то точки в разных городах, я просто не использую так, и подумал что вы для новичков делаете часы… что бы они просто подключили, и делали что угодно с кодом
Какой дрон по wifi и зачем ему местное время? Задумывалось для автономных систем автоматизации, но может тоже не нужно, судя по тому что никто не заинтересовался.
ну это тоже как вариант))) из за разницы во времени иногда интернет может не работать…
тогда точно нужны часы для людей!
Вы сейчас дискутируете с AI. если что. ну, может, не знали :). А BABOS - пророк его.
не знал. А что он тогда такие детские ошибки лепит?