diff --git a/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N16R8_app.bin b/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N16R8_app.bin index 64b597f..32df122 100644 Binary files a/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N16R8_app.bin and b/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N16R8_app.bin differ diff --git a/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N16R8_full.bin b/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N16R8_full.bin index e1ccec4..c4a7f79 100644 Binary files a/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N16R8_full.bin and b/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N16R8_full.bin differ diff --git a/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N4R2_app.bin b/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N4R2_app.bin index 3902b52..decb34d 100644 Binary files a/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N4R2_app.bin and b/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N4R2_app.bin differ diff --git a/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N4R2_full.bin b/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N4R2_full.bin index d5f213d..ad51531 100644 Binary files a/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N4R2_full.bin and b/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N4R2_full.bin differ diff --git a/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N8R2_app.bin b/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N8R2_app.bin index b3f4aa5..6a12a4f 100644 Binary files a/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N8R2_app.bin and b/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N8R2_app.bin differ diff --git a/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N8R2_full.bin b/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N8R2_full.bin index e9a0658..67aac96 100644 Binary files a/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N8R2_full.bin and b/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N8R2_full.bin differ diff --git a/src/web/web_server.cpp b/src/web/web_server.cpp index 479d6ca..f0097d9 100644 --- a/src/web/web_server.cpp +++ b/src/web/web_server.cpp @@ -194,19 +194,10 @@ static void handle_nvs_get() { } static void handle_nvs_save() { - if (!s_server.hasArg("plain")) { - s_server.send(400, "application/json", "{\"error\":\"missing_body\"}"); - return; - } - String err; - bool ok = nvs_manager_apply_json(s_server.arg("plain"), err); - if (ok) { - s_server.send(200, "application/json", "{\"status\":\"saved\",\"message\":\"NVS配置已成功更新并写入Flash!\"}"); - } else { - s_server.send(400, "application/json", String("{\"error\":\"") + err + "\"}"); - } + s_server.send(403, "application/json", "{\"error\":\"disabled\",\"message\":\"全量 NVS 直接回写功能已被禁用,按键配置请使用 /api/keymap/save 导入。\"}"); } + static void handle_nvs_reset() { bool ok = nvs_manager_erase_all(); if (ok) { diff --git a/src/web/web_ui.h b/src/web/web_ui.h index 97c1e56..e99bb83 100644 --- a/src/web/web_ui.h +++ b/src/web/web_ui.h @@ -328,7 +328,7 @@ const char INDEX_HTML[] PROGMEM = R"rawliteral( - + @@ -511,53 +511,110 @@ const char INDEX_HTML[] PROGMEM = R"rawliteral( - -
+ +
+
-
+
- NVS 配置管理 + 按键映射与层级配置备份
- 以 JSON 形式统一管理存储在 Flash NVS 中的所有持久化参数。 + 仅导入或导出 5 个层级的按键映射规则(Keymap),不包含 Wi-Fi、蓝牙配对及底层系统参数,安全无冲突。
-
- - - - - - - -
- -
-
- JSON 格式有效 -
- -
-
-
- NVS 命名空间说明 - • wifi_conf: Wi-Fi 与热点连接参数
- • ble_conf: 绑定的蓝牙遥控器参数
- • keymap_conf: 各层级按键映射规则字典
- • 保存后将持久化写入 Flash 中。 -
-
+
- 清空 NVS / 恢复出厂设置 - 擦除 Flash NVS 分区中的所有存储数据(清除所有 Wi-Fi、蓝牙配对和改键设置),并自动重启。 + 导出配置 +

+ 将当前 ESP32 的所有按键映射及层级设置导出为 .json 文件,用于本地备份或多设备间快速复制。 +

+
+
+ +
+
+ +
+
+ 导入配置 +

+ 从本地 JSON 文件恢复按键层级配置。系统将校验数据完整性,仅覆盖按键映射,绝不影响 Wi-Fi 与蓝牙绑定状态。 +

+
+
+ +
-
+ + +
+
+
+ 配置重置与清空 +
+ 提供按键层级单独重置与系统出厂完全重置两种方式,请仔细区分后执行。 +
+
+
+
+
+
+ 清空按键映射与层级数据 +

+ 将所有 5 个层级的自定义映射规则恢复为出厂默认。
+ 保留所有 Wi-Fi 密码、AP 设置以及已绑定的蓝牙遥控器连接。 +

+
+
+ +
+
+ +
+
+ 清空全部 NVS 数据(出厂完全重置) +

+ 彻底擦除 Flash NVS 内部的全部持久化分区。
+ 所有网络、蓝牙遥控器配对与按键映射将全部丢失,设备将立即自动重启。 +

+
+
+ +
+
+
+
+ + +
+
+ +
+ ▶ + 只读调试查看(仅查看纯文本格式的 NVS 内部数据) + 纯只读 +
+ 点击展开 / 折叠 +
+
+
+
+ 当前 ESP32 Flash NVS 中读取到的内部键值快照(含底层驱动与系统数据)。该区域严格只读,不允许直接修改或回写。 +
+
+ + +
+
+
点击上方展开即可加载 NVS 内部数据快照...
+
+
+
@@ -954,9 +1011,6 @@ const char INDEX_HTML[] PROGMEM = R"rawliteral( document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); event.target.classList.add('active'); document.getElementById(id).classList.add('active'); - if (id === 'tab-nvs') { - loadNvsConfig(); - } } async function fetchStatus() { @@ -2080,148 +2134,132 @@ const char INDEX_HTML[] PROGMEM = R"rawliteral( }, 3000); } - async function loadNvsConfig() { + // Config Management: Export Keymap + async function exportKeymapConfig() { try { - const res = await fetch('/api/nvs'); - const json = await res.json(); - const editor = document.getElementById('nvs-editor'); - editor.value = JSON.stringify(json, null, 2); - validateNvsJson(); - showToast('NVS 配置已从 Flash 加载'); + const res = await fetch('/api/keymap'); + const data = await res.json(); + const jsonStr = JSON.stringify(data, null, 2); + const blob = new Blob([jsonStr], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + const now = new Date(); + const pad = n => String(n).padStart(2, '0'); + const dateStr = `${now.getFullYear()}${pad(now.getMonth()+1)}${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`; + a.href = url; + a.download = `RemoteMapper_Keymap_${dateStr}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + showToast('按键与层级配置已成功导出!'); } catch(e) { - showToast('读取 NVS 失败: ' + e.message, true); + showToast('导出配置失败: ' + e.message, true); } } - function validateNvsJson() { - const editor = document.getElementById('nvs-editor'); - const badge = document.getElementById('nvs-json-status'); - if (!editor || !badge) return false; - try { - JSON.parse(editor.value); - badge.innerHTML = 'JSON 格式有效'; - badge.style.background = 'rgba(16,185,129,0.2)'; - badge.style.color = '#34d399'; - badge.style.borderColor = 'rgba(16,185,129,0.4)'; - return true; - } catch(e) { - badge.innerHTML = e.message; - badge.style.background = 'rgba(239,68,68,0.2)'; - badge.style.color = '#f87171'; - badge.style.borderColor = 'rgba(239,68,68,0.4)'; - return false; - } - } - - function formatNvsJson() { - const editor = document.getElementById('nvs-editor'); - try { - const obj = JSON.parse(editor.value); - editor.value = JSON.stringify(obj, null, 2); - validateNvsJson(); - showToast('JSON 格式化完成'); - } catch(e) { - showToast('无法格式化,当前 JSON 存在语法错误: ' + e.message, true); - } - } - - function copyNvsJson() { - const editor = document.getElementById('nvs-editor'); - navigator.clipboard.writeText(editor.value).then(() => { - showToast('已复制 NVS JSON 到剪贴板'); - }).catch(() => { - editor.select(); - document.execCommand('copy'); - showToast('已复制到剪贴板'); - }); - } - - function exportNvsJson() { - const editor = document.getElementById('nvs-editor'); - const blob = new Blob([editor.value], { type: 'application/json' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - const timeStr = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); - a.href = url; - a.download = `RemoteMapper_NVS_Backup_${timeStr}.json`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - showToast('NVS 备份文件已导出下载'); - } - - function importNvsJson(e) { + // Config Management: Import Keymap + function importKeymapConfig(e) { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); - reader.onload = function(evt) { + reader.onload = async function(evt) { try { - const obj = JSON.parse(evt.target.result); - document.getElementById('nvs-editor').value = JSON.stringify(obj, null, 2); - validateNvsJson(); - showToast('文件导入成功,请点击“保存写入 NVS”生效'); + const data = JSON.parse(evt.target.result); + if (!data || !Array.isArray(data.layers)) { + throw new Error('无效的按键配置格式(必须包含 layers 数组)'); + } + if (!confirm(`确定要导入按键配置文件「${file.name}」吗?\n\n导入后将覆盖所有按键映射与层级设置,保留现有的 Wi-Fi 与蓝牙配置。`)) { + return; + } + const res = await fetch('/api/keymap/save', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }); + const ret = await res.json(); + if (res.ok) { + showToast('按键配置导入成功并已保存!'); + await loadKeymap(); + } else { + showToast('导入失败: ' + (ret.error || '服务器拒绝该配置'), true); + } } catch(err) { - showToast('导入文件解析失败: ' + err.message, true); + showToast('导入解析失败: ' + err.message, true); + } finally { + e.target.value = ''; } }; reader.readAsText(file); - e.target.value = ''; } - async function saveNvsConfig() { - const editor = document.getElementById('nvs-editor'); - if (!validateNvsJson()) { - showToast('当前 JSON 存在语法错误,无法保存!', true); - return; - } - if (!confirm('确定要将当前编辑的 JSON 写入 ESP32 Flash NVS 吗?\n写入后参数将立即更新。')) { + // Config Management: Reset Keymap Only (preserve WiFi & BLE) + async function resetKeymapOnly() { + if (!confirm('确定要清空所有按键映射与层级设置吗?\n\n所有按键将恢复为出厂默认映射规则。\n蓝牙遥控器配对与 Wi-Fi 网络参数将完全保留!')) { return; } try { - const res = await fetch('/api/nvs/save', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: editor.value - }); - const ret = await res.json(); + const res = await fetch('/api/keymap/reset', { method: 'POST' }); if (res.ok) { - showToast(ret.message || 'NVS 配置已成功保存到 Flash!'); - loadKeymap(); + await loadKeymap(); + showToast('按键与层级数据已恢复出厂默认(蓝牙与Wi-Fi配置已保留)'); } else { - showToast('保存失败: ' + (ret.error || '未知错误'), true); + showToast('重置按键映射失败', true); } } catch(e) { - showToast('保存请求失败: ' + e.message, true); + showToast('请求失败: ' + e.message, true); } } + // Config Management: Factory Reset (wipe all NVS) async function resetNvsFactory() { - if (!confirm('警告:此操作将清空 ESP32 内部的所有 NVS 持久化数据(包括 Wi-Fi 密码、遥控器绑定、自定义按键),并自动重启!\n\n确定要执行出厂重置吗?')) { + if (!confirm('⚠️ 严重警告:此操作将清空 ESP32 内部的全部 NVS 持久化数据!\n\n包括:\n• 所有 Wi-Fi 密码与 AP 热点设置\n• 绑定的蓝牙遥控器配对信息\n• 所有自定义按键与多层级映射\n\n设备将自动重启并完全恢复出厂设置!是否继续?')) { return; } try { const res = await fetch('/api/nvs/reset', { method: 'POST' }); const ret = await res.json(); - showToast(ret.message || 'NVS 已清空,设备重启中...'); - setTimeout(() => location.reload(), 3000); + showToast(ret.message || '全部 NVS 已清空,设备重启中...'); + setTimeout(() => location.reload(), 3500); } catch(e) { showToast('操作失败: ' + e.message, true); } } - function handleNvsEditorKey(e) { - if (e.key === 'Tab') { - e.preventDefault(); - const textarea = e.target; - const start = textarea.selectionStart; - const end = textarea.selectionEnd; - textarea.value = textarea.value.substring(0, start) + ' ' + textarea.value.substring(end); - textarea.selectionStart = textarea.selectionEnd = start + 2; - validateNvsJson(); + // Config Management: Read-Only NVS Debug Viewer + function onNvsDebugToggle(detailsEl) { + const arrow = document.getElementById('nvs-details-arrow'); + if (arrow) arrow.innerText = detailsEl.open ? '▼' : '▶'; + if (detailsEl.open) { + loadNvsReadOnly(false); } } + async function loadNvsReadOnly(forceToast = false) { + const viewer = document.getElementById('nvs-readonly-viewer'); + if (!viewer) return; + if (forceToast) viewer.textContent = '正在刷新 NVS 内部数据...'; + try { + const res = await fetch('/api/nvs'); + const json = await res.json(); + viewer.textContent = JSON.stringify(json, null, 2); + if (forceToast) showToast('NVS 内部数据已刷新'); + } catch(e) { + viewer.textContent = '读取 NVS 数据失败: ' + e.message; + if (forceToast) showToast('读取 NVS 失败: ' + e.message, true); + } + } + + function copyNvsReadOnly() { + const viewer = document.getElementById('nvs-readonly-viewer'); + if (!viewer) return; + navigator.clipboard.writeText(viewer.textContent).then(() => { + showToast('已复制 NVS 只读数据到剪贴板'); + }).catch(() => { + showToast('复制失败,请手动选取文本'); + }); + } + // Periodic background pollers setInterval(fetchKeyTelemetry, 100); setInterval(fetchStatus, 3000);