Files
Watch-OS/hal/esp32/wifi_server.cpp
Kazimierz Ciołek f17984820f Initialize git
2026-02-25 01:56:31 +01:00

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 */