356 lines
10 KiB
C++
356 lines
10 KiB
C++
/**
|
|
* @file wifi_server.cpp
|
|
* @brief WiFi Access Point + HTTP WebServer for ESP32 Watch
|
|
* Provides time sync and notes management via web interface
|
|
*/
|
|
|
|
#include "app_hal.h"
|
|
|
|
#ifdef ENABLE_WIFI_SERVER
|
|
|
|
#include <WiFi.h>
|
|
#include <WebServer.h>
|
|
#include <ArduinoJson.h>
|
|
#include <ChronosESP32.h>
|
|
#include "wifi_server.h"
|
|
#include "../src/apps/notes/notes_storage.h"
|
|
|
|
/* WiFi AP Configuration */
|
|
#define WIFI_SSID "ESP32-Watch"
|
|
#define WIFI_PASS "12345678"
|
|
|
|
/* External references */
|
|
extern ChronosESP32 watch;
|
|
|
|
static WebServer server(80);
|
|
static bool serverRunning = false;
|
|
|
|
/* ──────────────────────────────────────────────
|
|
* Embedded HTML Page (PROGMEM)
|
|
* ────────────────────────────────────────────── */
|
|
|
|
static const char index_html[] PROGMEM = R"rawliteral(
|
|
<!DOCTYPE html>
|
|
<html lang="pl">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>ESP32 Watch Manager</title>
|
|
<style>
|
|
*{box-sizing:border-box;margin:0;padding:0}
|
|
body{font-family:'Segoe UI',system-ui,sans-serif;background:#0a0a1a;color:#e0e0e0;min-height:100vh;padding:16px}
|
|
.container{max-width:600px;margin:0 auto}
|
|
h1{text-align:center;color:#ffcc00;margin-bottom:24px;font-size:1.5em}
|
|
h2{color:#ffcc00;margin-bottom:12px;font-size:1.1em;border-bottom:1px solid #333;padding-bottom:6px}
|
|
.card{background:#1a1a2e;border-radius:12px;padding:16px;margin-bottom:16px}
|
|
.btn{background:#ffcc00;color:#0a0a1a;border:none;padding:10px 20px;border-radius:8px;cursor:pointer;font-weight:600;font-size:0.95em;transition:background 0.2s}
|
|
.btn:hover{background:#ffd633}
|
|
.btn-danger{background:#cc3333;color:#fff}
|
|
.btn-danger:hover{background:#ff4444}
|
|
.btn-sm{padding:6px 12px;font-size:0.85em}
|
|
input,textarea{width:100%;padding:10px;border-radius:8px;border:1px solid #333;background:#0d0d20;color:#e0e0e0;font-size:0.95em;margin-bottom:8px;font-family:inherit}
|
|
input:focus,textarea:focus{outline:none;border-color:#ffcc00}
|
|
textarea{resize:vertical;min-height:80px}
|
|
.note-item{background:#0d0d20;border-radius:8px;padding:12px;margin-bottom:8px;position:relative}
|
|
.note-item h3{color:#ffcc00;font-size:0.95em;margin-bottom:4px}
|
|
.note-item p{color:#aaa;font-size:0.9em;white-space:pre-wrap;word-break:break-word}
|
|
.note-item .del-btn{position:absolute;top:8px;right:8px}
|
|
.status{text-align:center;padding:8px;color:#88ff88;font-size:0.9em;min-height:28px}
|
|
.status.error{color:#ff8888}
|
|
.info{display:flex;justify-content:space-between;flex-wrap:wrap;gap:8px;font-size:0.9em;color:#888;margin-bottom:8px}
|
|
.sync-row{display:flex;align-items:center;gap:12px;flex-wrap:wrap}
|
|
.sync-row .btn{flex-shrink:0}
|
|
#clock{font-size:1.2em;color:#ffcc00;font-weight:600}
|
|
.note-count{color:#888;font-size:0.85em;margin-bottom:8px}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1>ESP32 Watch Manager</h1>
|
|
|
|
<div class="card">
|
|
<h2>Czas</h2>
|
|
<div class="sync-row">
|
|
<span id="clock">--:--:--</span>
|
|
<button class="btn" onclick="syncTime()">Synchronizuj czas</button>
|
|
</div>
|
|
<div class="status" id="timeStatus"></div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2>Notatki</h2>
|
|
<div class="note-count" id="noteCount"></div>
|
|
<div id="notesList"></div>
|
|
<hr style="border-color:#333;margin:12px 0">
|
|
<h3 style="color:#ccc;font-size:0.95em;margin-bottom:8px">Dodaj notatke</h3>
|
|
<input type="text" id="newTitle" placeholder="Tytul" maxlength="63">
|
|
<textarea id="newContent" placeholder="Tresc notatki..." maxlength="511"></textarea>
|
|
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
|
<button class="btn" onclick="addNote()">Dodaj</button>
|
|
<button class="btn btn-danger" onclick="clearAll()">Usun wszystkie</button>
|
|
</div>
|
|
<div class="status" id="notesStatus"></div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2>Status</h2>
|
|
<div class="info">
|
|
<span>IP: 192.168.4.1</span>
|
|
<span id="batteryInfo">Bateria: --</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let notes = [];
|
|
|
|
async function fetchJson(url, opts) {
|
|
try {
|
|
const r = await fetch(url, opts);
|
|
return await r.json();
|
|
} catch(e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function loadNotes() {
|
|
const data = await fetchJson('/api/notes');
|
|
if (data) {
|
|
notes = data;
|
|
renderNotes();
|
|
}
|
|
}
|
|
|
|
function renderNotes() {
|
|
const el = document.getElementById('notesList');
|
|
const cnt = document.getElementById('noteCount');
|
|
cnt.textContent = notes.length + ' notatek';
|
|
if (notes.length === 0) {
|
|
el.innerHTML = '<p style="color:#666;text-align:center;padding:12px">Brak notatek</p>';
|
|
return;
|
|
}
|
|
el.innerHTML = notes.map((n, i) =>
|
|
'<div class="note-item"><h3>' + esc(n.title) + '</h3><p>' + esc(n.content) + '</p>' +
|
|
'<button class="btn btn-danger btn-sm del-btn" onclick="delNote(' + i + ')">X</button></div>'
|
|
).join('');
|
|
}
|
|
|
|
function esc(s) {
|
|
const d = document.createElement('div');
|
|
d.textContent = s;
|
|
return d.innerHTML;
|
|
}
|
|
|
|
async function saveNotes() {
|
|
const r = await fetch('/api/notes', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify(notes)
|
|
});
|
|
const j = await r.json();
|
|
showStatus('notesStatus', j.ok ? 'Zapisano!' : 'Blad zapisu', !j.ok);
|
|
}
|
|
|
|
async function addNote() {
|
|
const t = document.getElementById('newTitle').value.trim();
|
|
const c = document.getElementById('newContent').value.trim();
|
|
if (!t && !c) { showStatus('notesStatus', 'Wpisz tytul lub tresc', true); return; }
|
|
notes.push({title: t || 'Notatka', content: c});
|
|
await saveNotes();
|
|
loadNotes();
|
|
document.getElementById('newTitle').value = '';
|
|
document.getElementById('newContent').value = '';
|
|
}
|
|
|
|
async function delNote(i) {
|
|
notes.splice(i, 1);
|
|
await saveNotes();
|
|
renderNotes();
|
|
}
|
|
|
|
async function clearAll() {
|
|
if (!confirm('Usunac wszystkie notatki?')) return;
|
|
notes = [];
|
|
await saveNotes();
|
|
renderNotes();
|
|
}
|
|
|
|
async function syncTime() {
|
|
const now = new Date();
|
|
const body = {
|
|
h: now.getHours(), m: now.getMinutes(), s: now.getSeconds(),
|
|
d: now.getDate(), mo: now.getMonth() + 1, y: now.getFullYear()
|
|
};
|
|
const r = await fetch('/api/time', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify(body)
|
|
});
|
|
const j = await r.json();
|
|
showStatus('timeStatus', j.ok ? 'Czas zsynchronizowany!' : 'Blad synchronizacji', !j.ok);
|
|
if (j.ok) updateClock();
|
|
}
|
|
|
|
function showStatus(id, msg, isErr) {
|
|
const el = document.getElementById(id);
|
|
el.textContent = msg;
|
|
el.className = 'status' + (isErr ? ' error' : '');
|
|
setTimeout(() => { el.textContent = ''; }, 3000);
|
|
}
|
|
|
|
async function updateClock() {
|
|
const data = await fetchJson('/api/status');
|
|
if (data) {
|
|
document.getElementById('clock').textContent =
|
|
String(data.h).padStart(2,'0') + ':' +
|
|
String(data.m).padStart(2,'0') + ':' +
|
|
String(data.s).padStart(2,'0');
|
|
document.getElementById('batteryInfo').textContent = 'Bateria: ' + data.bat + '%';
|
|
}
|
|
}
|
|
|
|
setInterval(updateClock, 2000);
|
|
updateClock();
|
|
loadNotes();
|
|
</script>
|
|
</body>
|
|
</html>
|
|
)rawliteral";
|
|
|
|
/* ──────────────────────────────────────────────
|
|
* API Handlers
|
|
* ────────────────────────────────────────────── */
|
|
|
|
static void handleRoot()
|
|
{
|
|
server.send(200, "text/html", index_html);
|
|
}
|
|
|
|
static void handleGetNotes()
|
|
{
|
|
char *json = notes_storage_get_json();
|
|
if (json) {
|
|
server.send(200, "application/json", json);
|
|
free(json);
|
|
} else {
|
|
server.send(200, "application/json", "[]");
|
|
}
|
|
}
|
|
|
|
static void handlePostNotes()
|
|
{
|
|
if (!server.hasArg("plain")) {
|
|
server.send(400, "application/json", "{\"ok\":false,\"error\":\"no body\"}");
|
|
return;
|
|
}
|
|
|
|
String body = server.arg("plain");
|
|
bool ok = notes_storage_save(body.c_str());
|
|
|
|
if (ok) {
|
|
server.send(200, "application/json", "{\"ok\":true}");
|
|
} else {
|
|
server.send(500, "application/json", "{\"ok\":false,\"error\":\"write failed\"}");
|
|
}
|
|
}
|
|
|
|
static void handlePostTime()
|
|
{
|
|
if (!server.hasArg("plain")) {
|
|
server.send(400, "application/json", "{\"ok\":false,\"error\":\"no body\"}");
|
|
return;
|
|
}
|
|
|
|
String body = server.arg("plain");
|
|
JsonDocument doc;
|
|
DeserializationError error = deserializeJson(doc, body);
|
|
|
|
if (error) {
|
|
server.send(400, "application/json", "{\"ok\":false,\"error\":\"invalid json\"}");
|
|
return;
|
|
}
|
|
|
|
int h = doc["h"] | 0;
|
|
int m = doc["m"] | 0;
|
|
int s = doc["s"] | 0;
|
|
int d = doc["d"] | 1;
|
|
int mo = doc["mo"] | 1;
|
|
int y = doc["y"] | 2025;
|
|
|
|
watch.setTime(s, m, h, d, mo, y);
|
|
|
|
server.send(200, "application/json", "{\"ok\":true}");
|
|
}
|
|
|
|
static void handleGetStatus()
|
|
{
|
|
JsonDocument doc;
|
|
doc["h"] = watch.getHour(true);
|
|
doc["m"] = watch.getMinute();
|
|
doc["s"] = watch.getSecond();
|
|
doc["d"] = watch.getDay();
|
|
doc["mo"] = watch.getMonth() + 1;
|
|
doc["y"] = watch.getYear();
|
|
doc["bat"] = watch.getPhoneBattery();
|
|
|
|
String output;
|
|
serializeJson(doc, output);
|
|
server.send(200, "application/json", output);
|
|
}
|
|
|
|
static void handleNotFound()
|
|
{
|
|
server.send(404, "text/plain", "Not Found");
|
|
}
|
|
|
|
/* ──────────────────────────────────────────────
|
|
* Public API
|
|
* ────────────────────────────────────────────── */
|
|
|
|
extern "C" {
|
|
|
|
void wifi_server_start(void)
|
|
{
|
|
if (serverRunning) return;
|
|
|
|
WiFi.mode(WIFI_AP);
|
|
WiFi.softAP(WIFI_SSID, WIFI_PASS);
|
|
|
|
/* Register endpoints */
|
|
server.on("/", HTTP_GET, handleRoot);
|
|
server.on("/api/notes", HTTP_GET, handleGetNotes);
|
|
server.on("/api/notes", HTTP_POST, handlePostNotes);
|
|
server.on("/api/time", HTTP_POST, handlePostTime);
|
|
server.on("/api/status", HTTP_GET, handleGetStatus);
|
|
server.onNotFound(handleNotFound);
|
|
|
|
server.begin();
|
|
serverRunning = true;
|
|
}
|
|
|
|
void wifi_server_stop(void)
|
|
{
|
|
if (!serverRunning) return;
|
|
|
|
server.stop();
|
|
WiFi.softAPdisconnect(true);
|
|
WiFi.mode(WIFI_OFF);
|
|
serverRunning = false;
|
|
}
|
|
|
|
void wifi_server_handle(void)
|
|
{
|
|
if (serverRunning) {
|
|
server.handleClient();
|
|
}
|
|
}
|
|
|
|
bool wifi_server_is_running(void)
|
|
{
|
|
return serverRunning;
|
|
}
|
|
|
|
} /* extern "C" */
|
|
|
|
#endif /* ENABLE_WIFI_SERVER */
|