feat(travel): Implement settings UI and web server for MTR route configuration
- Added MainUIHandler class to manage the main UI and polling for arrival data. - Introduced SettingsUI class for displaying QR code and configuration options. - Created SettingsUIHandler to manage settings UI lifecycle and web server interactions. - Developed WebHandler to handle HTTP requests for MTR route settings, including adding and removing routes. - Implemented web endpoints for fetching MTR lines, routes, and saving settings. - Enhanced UI with responsive design for e-ink displays and added error handling for web interactions.
This commit is contained in:
718
main/ui/apps/travel/web/web_handlers.cpp
Normal file
718
main/ui/apps/travel/web/web_handlers.cpp
Normal file
@@ -0,0 +1,718 @@
|
||||
#include "ui/apps/travel/web/web_handlers.h"
|
||||
#include "esp_log.h"
|
||||
#include "cJSON.h"
|
||||
#include <cstring>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
|
||||
static const char* TAG = "TravelWebHandler";
|
||||
|
||||
namespace travel {
|
||||
|
||||
WebHandler::WebHandler(
|
||||
SettingHandler* setting_handler,
|
||||
NetworkHandler* network_handler
|
||||
)
|
||||
: web_server_(std::make_unique<WebServerHandler>())
|
||||
, setting_handler_(setting_handler)
|
||||
, network_handler_(network_handler)
|
||||
, mtr_handler_(std::make_unique<MTRNextTrainHandler>())
|
||||
, auth_key_(generate_auth_key_()) {
|
||||
}
|
||||
|
||||
WebHandler::~WebHandler() {
|
||||
stop_web_server();
|
||||
}
|
||||
|
||||
std::string WebHandler::generate_auth_key_() {
|
||||
// Generate a random 16-character hex key
|
||||
std::stringstream ss;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
ss << std::hex << std::setw(2) << std::setfill('0') << (esp_random() & 0xFF);
|
||||
}
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
esp_err_t WebHandler::start_web_server() {
|
||||
uint16_t port = web_server_->start(auth_key_, WEB_SERVER_PORT);
|
||||
if (port == 0) {
|
||||
ESP_LOGE(TAG, "Failed to start web server");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
esp_err_t err = register_web_endpoints_();
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to register endpoints: %s", esp_err_to_name(err));
|
||||
web_server_->stop();
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Web server started on port %d", port);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t WebHandler::stop_web_server() {
|
||||
if (web_server_) {
|
||||
web_server_->stop();
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
std::string WebHandler::get_url() const {
|
||||
std::string ip = get_device_ip();
|
||||
if (ip.empty()) {
|
||||
return "";
|
||||
}
|
||||
return "http://" + ip + ":" + std::to_string(WEB_SERVER_PORT) + "/?auth=" + auth_key_;
|
||||
}
|
||||
|
||||
std::string WebHandler::get_device_ip() const {
|
||||
if (!network_handler_) {
|
||||
return "";
|
||||
}
|
||||
return network_handler_->get_wifi_handler().get_current_ip();
|
||||
}
|
||||
|
||||
uint16_t WebHandler::get_port() const {
|
||||
return WEB_SERVER_PORT;
|
||||
}
|
||||
|
||||
esp_err_t WebHandler::register_web_endpoints_() {
|
||||
// Main settings page
|
||||
httpd_uri_t settings_uri = {
|
||||
.uri = "/",
|
||||
.method = HTTP_GET,
|
||||
.handler = settings_page_handler_,
|
||||
.user_ctx = this
|
||||
};
|
||||
ESP_ERROR_CHECK(web_server_->register_uri_handler(&settings_uri));
|
||||
|
||||
// Get MTR lines
|
||||
httpd_uri_t lines_uri = {
|
||||
.uri = "/api/lines",
|
||||
.method = HTTP_GET,
|
||||
.handler = get_lines_handler_,
|
||||
.user_ctx = this
|
||||
};
|
||||
ESP_ERROR_CHECK(web_server_->register_uri_handler(&lines_uri));
|
||||
|
||||
// Get saved routes
|
||||
httpd_uri_t routes_uri = {
|
||||
.uri = "/api/routes",
|
||||
.method = HTTP_GET,
|
||||
.handler = get_routes_handler_,
|
||||
.user_ctx = this
|
||||
};
|
||||
ESP_ERROR_CHECK(web_server_->register_uri_handler(&routes_uri));
|
||||
|
||||
// Add route
|
||||
httpd_uri_t add_route_uri = {
|
||||
.uri = "/api/routes",
|
||||
.method = HTTP_POST,
|
||||
.handler = add_route_handler_,
|
||||
.user_ctx = this
|
||||
};
|
||||
ESP_ERROR_CHECK(web_server_->register_uri_handler(&add_route_uri));
|
||||
|
||||
// Remove route
|
||||
httpd_uri_t remove_route_uri = {
|
||||
.uri = "/api/routes",
|
||||
.method = HTTP_DELETE,
|
||||
.handler = remove_route_handler_,
|
||||
.user_ctx = this
|
||||
};
|
||||
ESP_ERROR_CHECK(web_server_->register_uri_handler(&remove_route_uri));
|
||||
|
||||
// Save settings (polling interval)
|
||||
httpd_uri_t save_uri = {
|
||||
.uri = "/api/settings",
|
||||
.method = HTTP_POST,
|
||||
.handler = save_settings_handler_,
|
||||
.user_ctx = this
|
||||
};
|
||||
ESP_ERROR_CHECK(web_server_->register_uri_handler(&save_uri));
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t WebHandler::settings_page_handler_(httpd_req_t* req) {
|
||||
WebHandler* handler = static_cast<WebHandler*>(req->user_ctx);
|
||||
|
||||
// Check auth
|
||||
char auth_param[33] = {0};
|
||||
if (httpd_req_get_url_query_str(req, auth_param, sizeof(auth_param)) == ESP_OK) {
|
||||
char auth_value[33] = {0};
|
||||
if (httpd_query_key_value(auth_param, "auth", auth_value, sizeof(auth_value)) == ESP_OK) {
|
||||
if (handler->auth_key_ != auth_value) {
|
||||
httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Unauthorized");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
} else {
|
||||
httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Missing auth");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
} else {
|
||||
httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Missing auth");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
// HTML page with inline CSS and JavaScript
|
||||
const char* html = R"html(
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MTR Travel Settings</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
h1 { font-size: 24px; margin-bottom: 20px; color: #333; }
|
||||
h2 { font-size: 18px; margin: 20px 0 10px; color: #555; }
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
margin-bottom: 15px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
.form-group { margin-bottom: 15px; }
|
||||
label { display: block; margin-bottom: 5px; font-weight: 500; color: #555; }
|
||||
select, input[type="number"] {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
}
|
||||
button {
|
||||
background: #007DC5;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 20px;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
}
|
||||
button:hover { background: #005a8c; }
|
||||
button.secondary {
|
||||
background: #6c757d;
|
||||
}
|
||||
button.danger {
|
||||
background: #dc3545;
|
||||
}
|
||||
.route-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.route-info { flex: 1; }
|
||||
.route-line {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
.status { margin-top: 10px; padding: 10px; border-radius: 4px; }
|
||||
.status.success { background: #d4edda; color: #155724; }
|
||||
.status.error { background: #f8d7da; color: #721c24; }
|
||||
.slider-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
input[type="range"] { flex: 1; }
|
||||
.value-display { min-width: 60px; text-align: right; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>MTR Travel 設定</h1>
|
||||
|
||||
<div class="card">
|
||||
<h2>新增路線</h2>
|
||||
<div class="form-group">
|
||||
<label>路線</label>
|
||||
<select id="line_select" onchange="updateStations()">
|
||||
<option value="">請選擇路線</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>出發站</label>
|
||||
<select id="station_select">
|
||||
<option value="">請先選擇路線</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>目的地</label>
|
||||
<select id="dest_select">
|
||||
<option value="">請先選擇路線</option>
|
||||
</select>
|
||||
</div>
|
||||
<button onclick="addRoute()">新增路線</button>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>已儲存路線</h2>
|
||||
<div id="routes_list"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>更新頻率</h2>
|
||||
<div class="form-group">
|
||||
<label>資料更新間隔</label>
|
||||
<div class="slider-container">
|
||||
<input type="range" id="interval_slider" min="10" max="120" value="30" step="5"
|
||||
oninput="updateIntervalDisplay()">
|
||||
<span class="value-display"><span id="interval_value">30</span> 秒</span>
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="saveSettings()">儲存設定</button>
|
||||
</div>
|
||||
|
||||
<div id="status"></div>
|
||||
|
||||
<script>
|
||||
let linesData = [];
|
||||
let routesData = [];
|
||||
|
||||
// Load initial data
|
||||
async function init() {
|
||||
await loadLines();
|
||||
await loadRoutes();
|
||||
updateIntervalDisplay();
|
||||
}
|
||||
|
||||
async function loadLines() {
|
||||
try {
|
||||
const response = await fetch('/api/lines');
|
||||
linesData = await response.json();
|
||||
const select = document.getElementById('line_select');
|
||||
select.innerHTML = '<option value="">請選擇路線</option>';
|
||||
linesData.forEach(line => {
|
||||
const option = document.createElement('option');
|
||||
option.value = line.code;
|
||||
option.textContent = line.name;
|
||||
select.appendChild(option);
|
||||
});
|
||||
} catch (err) {
|
||||
showStatus('無法載入路線資料', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRoutes() {
|
||||
try {
|
||||
const response = await fetch('/api/routes');
|
||||
const data = await response.json();
|
||||
routesData = data.routes || [];
|
||||
document.getElementById('interval_slider').value = data.polling_interval || 30;
|
||||
renderRoutes();
|
||||
} catch (err) {
|
||||
showStatus('無法載入路線', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function updateStations() {
|
||||
const lineCode = document.getElementById('line_select').value;
|
||||
const stationSelect = document.getElementById('station_select');
|
||||
const destSelect = document.getElementById('dest_select');
|
||||
|
||||
if (!lineCode) {
|
||||
stationSelect.innerHTML = '<option value="">請先選擇路線</option>';
|
||||
destSelect.innerHTML = '<option value="">請先選擇路線</option>';
|
||||
return;
|
||||
}
|
||||
|
||||
const line = linesData.find(l => l.code === lineCode);
|
||||
if (!line) return;
|
||||
|
||||
const stationsHtml = line.stations.map(s =>
|
||||
`<option value="${s.code}">${s.name}</option>`
|
||||
).join('');
|
||||
|
||||
stationSelect.innerHTML = '<option value="">請選擇車站</option>' + stationsHtml;
|
||||
destSelect.innerHTML = '<option value="">請選擇目的地</option>' + stationsHtml;
|
||||
}
|
||||
|
||||
function renderRoutes() {
|
||||
const container = document.getElementById('routes_list');
|
||||
if (routesData.length === 0) {
|
||||
container.innerHTML = '<p style="color: #666;">尚未設定路線</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = routesData.map((route, index) => `
|
||||
<div class="route-item">
|
||||
<div class="route-info">
|
||||
<span class="route-line" style="background: ${route.line_color}">${route.line_name}</span>
|
||||
${route.station_name} → ${route.dest_name}
|
||||
</div>
|
||||
<button class="danger" style="width: auto; padding: 5px 10px;"
|
||||
onclick="removeRoute(${index})">刪除</button>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
async function addRoute() {
|
||||
const lineCode = document.getElementById('line_select').value;
|
||||
const stationCode = document.getElementById('station_select').value;
|
||||
const destCode = document.getElementById('dest_select').value;
|
||||
|
||||
if (!lineCode || !stationCode || !destCode) {
|
||||
showStatus('請選擇路線、出發站和目的地', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (stationCode === destCode) {
|
||||
showStatus('出發站和目的地不能相同', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const line = linesData.find(l => l.code === lineCode);
|
||||
const station = line.stations.find(s => s.code === stationCode);
|
||||
const dest = line.stations.find(s => s.code === destCode);
|
||||
|
||||
const route = {
|
||||
line_code: lineCode,
|
||||
line_name: line.name,
|
||||
line_color: line.color,
|
||||
station_code: stationCode,
|
||||
station_name: station.name,
|
||||
dest_code: destCode,
|
||||
dest_name: dest.name
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/routes', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(route)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showStatus('路線已新增', 'success');
|
||||
await loadRoutes();
|
||||
} else {
|
||||
const err = await response.text();
|
||||
showStatus('新增失敗: ' + err, 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showStatus('新增失敗', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function removeRoute(index) {
|
||||
try {
|
||||
const response = await fetch('/api/routes', {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ index: index })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showStatus('路線已刪除', 'success');
|
||||
await loadRoutes();
|
||||
} else {
|
||||
showStatus('刪除失敗', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showStatus('刪除失敗', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function updateIntervalDisplay() {
|
||||
const value = document.getElementById('interval_slider').value;
|
||||
document.getElementById('interval_value').textContent = value;
|
||||
}
|
||||
|
||||
async function saveSettings() {
|
||||
const interval = parseInt(document.getElementById('interval_slider').value);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/settings', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ polling_interval: interval })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showStatus('設定已儲存', 'success');
|
||||
} else {
|
||||
showStatus('儲存失敗', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showStatus('儲存失敗', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function showStatus(message, type) {
|
||||
const statusDiv = document.getElementById('status');
|
||||
statusDiv.className = 'status ' + type;
|
||||
statusDiv.textContent = message;
|
||||
setTimeout(() => {
|
||||
statusDiv.className = '';
|
||||
statusDiv.textContent = '';
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
)html";
|
||||
|
||||
httpd_resp_set_type(req, "text/html");
|
||||
httpd_resp_send(req, html, strlen(html));
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t WebHandler::get_lines_handler_(httpd_req_t* req) {
|
||||
WebHandler* handler = static_cast<WebHandler*>(req->user_ctx);
|
||||
|
||||
// Get all lines from MTR handler
|
||||
std::vector<LineInfo> lines = handler->mtr_handler_->get_lines();
|
||||
|
||||
cJSON* root = cJSON_CreateArray();
|
||||
for (const auto& line : lines) {
|
||||
cJSON* line_obj = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(line_obj, "code", line.code());
|
||||
cJSON_AddStringToObject(line_obj, "name", line.name());
|
||||
cJSON_AddStringToObject(line_obj, "color", line.color());
|
||||
|
||||
// Add stations
|
||||
cJSON* stations_arr = cJSON_CreateArray();
|
||||
const auto* stations = line.stations();
|
||||
if (stations) {
|
||||
for (const auto& station : *stations) {
|
||||
cJSON* station_obj = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(station_obj, "code", station.code());
|
||||
cJSON_AddStringToObject(station_obj, "name", station.name());
|
||||
cJSON_AddItemToArray(stations_arr, station_obj);
|
||||
}
|
||||
}
|
||||
cJSON_AddItemToObject(line_obj, "stations", stations_arr);
|
||||
|
||||
cJSON_AddItemToArray(root, line_obj);
|
||||
}
|
||||
|
||||
char* json_str = cJSON_PrintUnformatted(root);
|
||||
cJSON_Delete(root);
|
||||
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, json_str ? json_str : "[]", json_str ? strlen(json_str) : 2);
|
||||
|
||||
if (json_str) {
|
||||
free(json_str);
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t WebHandler::get_routes_handler_(httpd_req_t* req) {
|
||||
WebHandler* handler = static_cast<WebHandler*>(req->user_ctx);
|
||||
|
||||
cJSON* root = cJSON_CreateObject();
|
||||
|
||||
// Add routes
|
||||
cJSON* routes_arr = cJSON_CreateArray();
|
||||
const auto& routes = handler->setting_handler_->get_routes();
|
||||
for (const auto& route : routes) {
|
||||
cJSON* route_obj = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(route_obj, "line_code", route.line_code.c_str());
|
||||
cJSON_AddStringToObject(route_obj, "line_name", route.line_name.c_str());
|
||||
cJSON_AddStringToObject(route_obj, "line_color", route.line_color.c_str());
|
||||
cJSON_AddStringToObject(route_obj, "station_code", route.station_code.c_str());
|
||||
cJSON_AddStringToObject(route_obj, "station_name", route.station_name.c_str());
|
||||
cJSON_AddStringToObject(route_obj, "dest_code", route.dest_code.c_str());
|
||||
cJSON_AddStringToObject(route_obj, "dest_name", route.dest_name.c_str());
|
||||
cJSON_AddItemToArray(routes_arr, route_obj);
|
||||
}
|
||||
cJSON_AddItemToObject(root, "routes", routes_arr);
|
||||
|
||||
// Add polling interval
|
||||
cJSON_AddNumberToObject(root, "polling_interval", handler->setting_handler_->get_polling_interval());
|
||||
|
||||
char* json_str = cJSON_PrintUnformatted(root);
|
||||
cJSON_Delete(root);
|
||||
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, json_str ? json_str : "{}", json_str ? strlen(json_str) : 2);
|
||||
|
||||
if (json_str) {
|
||||
free(json_str);
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t WebHandler::add_route_handler_(httpd_req_t* req) {
|
||||
WebHandler* handler = static_cast<WebHandler*>(req->user_ctx);
|
||||
|
||||
// Read request body
|
||||
char buf[512];
|
||||
int received = 0;
|
||||
int remaining = req->content_len;
|
||||
|
||||
std::string body;
|
||||
while (remaining > 0) {
|
||||
received = httpd_req_recv(req, buf, std::min(remaining, (int)sizeof(buf) - 1));
|
||||
if (received <= 0) {
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Failed to read request");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
buf[received] = '\0';
|
||||
body += buf;
|
||||
remaining -= received;
|
||||
}
|
||||
|
||||
// Parse JSON
|
||||
cJSON* root = cJSON_Parse(body.c_str());
|
||||
if (!root) {
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
RoutePair route;
|
||||
cJSON* item;
|
||||
|
||||
item = cJSON_GetObjectItem(root, "line_code");
|
||||
if (item && cJSON_IsString(item)) route.line_code = item->valuestring;
|
||||
|
||||
item = cJSON_GetObjectItem(root, "line_name");
|
||||
if (item && cJSON_IsString(item)) route.line_name = item->valuestring;
|
||||
|
||||
item = cJSON_GetObjectItem(root, "line_color");
|
||||
if (item && cJSON_IsString(item)) route.line_color = item->valuestring;
|
||||
|
||||
item = cJSON_GetObjectItem(root, "station_code");
|
||||
if (item && cJSON_IsString(item)) route.station_code = item->valuestring;
|
||||
|
||||
item = cJSON_GetObjectItem(root, "station_name");
|
||||
if (item && cJSON_IsString(item)) route.station_name = item->valuestring;
|
||||
|
||||
item = cJSON_GetObjectItem(root, "dest_code");
|
||||
if (item && cJSON_IsString(item)) route.dest_code = item->valuestring;
|
||||
|
||||
item = cJSON_GetObjectItem(root, "dest_name");
|
||||
if (item && cJSON_IsString(item)) route.dest_name = item->valuestring;
|
||||
|
||||
cJSON_Delete(root);
|
||||
|
||||
if (route.line_code.empty() || route.station_code.empty() || route.dest_code.empty()) {
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Missing required fields");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
// Add route
|
||||
handler->setting_handler_->add_route(route);
|
||||
handler->setting_handler_->save_settings();
|
||||
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, "{\"success\":true}", 16);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t WebHandler::remove_route_handler_(httpd_req_t* req) {
|
||||
WebHandler* handler = static_cast<WebHandler*>(req->user_ctx);
|
||||
|
||||
// Read request body
|
||||
char buf[128];
|
||||
int received = 0;
|
||||
int remaining = req->content_len;
|
||||
|
||||
std::string body;
|
||||
while (remaining > 0) {
|
||||
received = httpd_req_recv(req, buf, std::min(remaining, (int)sizeof(buf) - 1));
|
||||
if (received <= 0) {
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Failed to read request");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
buf[received] = '\0';
|
||||
body += buf;
|
||||
remaining -= received;
|
||||
}
|
||||
|
||||
// Parse JSON
|
||||
cJSON* root = cJSON_Parse(body.c_str());
|
||||
if (!root) {
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
cJSON* index_item = cJSON_GetObjectItem(root, "index");
|
||||
if (!index_item || !cJSON_IsNumber(index_item)) {
|
||||
cJSON_Delete(root);
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Missing index");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
int index = index_item->valueint;
|
||||
cJSON_Delete(root);
|
||||
|
||||
handler->setting_handler_->remove_route(index);
|
||||
handler->setting_handler_->save_settings();
|
||||
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, "{\"success\":true}", 16);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t WebHandler::save_settings_handler_(httpd_req_t* req) {
|
||||
WebHandler* handler = static_cast<WebHandler*>(req->user_ctx);
|
||||
|
||||
// Read request body
|
||||
char buf[256];
|
||||
int received = 0;
|
||||
int remaining = req->content_len;
|
||||
|
||||
std::string body;
|
||||
while (remaining > 0) {
|
||||
received = httpd_req_recv(req, buf, std::min(remaining, (int)sizeof(buf) - 1));
|
||||
if (received <= 0) {
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Failed to read request");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
buf[received] = '\0';
|
||||
body += buf;
|
||||
remaining -= received;
|
||||
}
|
||||
|
||||
// Parse JSON
|
||||
cJSON* root = cJSON_Parse(body.c_str());
|
||||
if (!root) {
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
cJSON* interval_item = cJSON_GetObjectItem(root, "polling_interval");
|
||||
if (interval_item && cJSON_IsNumber(interval_item)) {
|
||||
uint32_t interval = interval_item->valueint;
|
||||
handler->setting_handler_->set_polling_interval(interval);
|
||||
}
|
||||
|
||||
cJSON_Delete(root);
|
||||
|
||||
handler->setting_handler_->save_settings();
|
||||
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, "{\"success\":true}", 16);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
} // namespace travel
|
||||
58
main/ui/apps/travel/web/web_handlers.h
Normal file
58
main/ui/apps/travel/web/web_handlers.h
Normal file
@@ -0,0 +1,58 @@
|
||||
#pragma once
|
||||
|
||||
#include "esp_http_server.h"
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include "network/web_server_handler.h"
|
||||
#include "ui/apps/travel/settings/settings_handler.h"
|
||||
#include "external/mtr/mtr.h"
|
||||
#include "network/network.h"
|
||||
|
||||
namespace travel {
|
||||
|
||||
/**
|
||||
* @brief HTTP request handlers for Travel app settings web interface
|
||||
*
|
||||
* These handlers serve the web configuration page for MTR routes.
|
||||
*/
|
||||
class WebHandler {
|
||||
public:
|
||||
WebHandler(
|
||||
SettingHandler* setting_handler,
|
||||
NetworkHandler* network_handler
|
||||
);
|
||||
~WebHandler();
|
||||
|
||||
esp_err_t start_web_server();
|
||||
esp_err_t stop_web_server();
|
||||
|
||||
std::string get_url() const;
|
||||
std::string get_device_ip() const;
|
||||
uint16_t get_port() const;
|
||||
|
||||
bool is_running() const {
|
||||
return web_server_ && web_server_->is_running();
|
||||
}
|
||||
|
||||
private:
|
||||
std::string generate_auth_key_();
|
||||
esp_err_t register_web_endpoints_();
|
||||
|
||||
// HTTP handlers
|
||||
static esp_err_t settings_page_handler_(httpd_req_t* req);
|
||||
static esp_err_t get_lines_handler_(httpd_req_t* req);
|
||||
static esp_err_t get_routes_handler_(httpd_req_t* req);
|
||||
static esp_err_t add_route_handler_(httpd_req_t* req);
|
||||
static esp_err_t remove_route_handler_(httpd_req_t* req);
|
||||
static esp_err_t save_settings_handler_(httpd_req_t* req);
|
||||
|
||||
std::unique_ptr<WebServerHandler> web_server_;
|
||||
SettingHandler* setting_handler_ = nullptr;
|
||||
NetworkHandler* network_handler_ = nullptr;
|
||||
std::unique_ptr<MTRNextTrainHandler> mtr_handler_;
|
||||
|
||||
std::string auth_key_;
|
||||
static constexpr uint16_t WEB_SERVER_PORT = 8081;
|
||||
};
|
||||
|
||||
} // namespace travel
|
||||
Reference in New Issue
Block a user