init config_server project

This commit is contained in:
cxh 2025-12-12 09:02:06 +08:00
commit 8decb5cf31
15 changed files with 38923 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
build/

29
CMakeLists.txt Normal file
View File

@ -0,0 +1,29 @@
cmake_minimum_required(VERSION 3.10)
project(demo_config_server LANGUAGES CXX)
# C++
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# ./bin
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin)
#
add_executable(config_server
src/config_server.cpp
)
# httplib/json
target_include_directories(config_server
PRIVATE
${CMAKE_SOURCE_DIR}/include
)
# 线httplib 线
find_package(Threads REQUIRED)
target_link_libraries(config_server
PRIVATE
Threads::Threads
)

296
bin/config.html Normal file
View File

@ -0,0 +1,296 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>设备配置界面</title>
<style>
body {
font-family: "Segoe UI", "Helvetica Neue", Arial;
padding: 30px;
background: #f5f7fa;
}
h2 {
margin-bottom: 20px;
color: #333;
}
.card {
background: white;
border-radius: 8px;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08);
margin-bottom: 18px;
overflow: hidden;
}
.card-header {
padding: 12px 16px;
background: linear-gradient(90deg, #e8ecf3, #dce3ee);
cursor: pointer;
user-select: none;
font-weight: bold;
font-size: 16px;
display: flex;
justify-content: space-between;
align-items: center;
}
.arrow {
font-size: 14px;
}
.card-body {
overflow: hidden;
height: auto;
transition: height 0.25s ease;
}
.content-wrapper {
padding: 15px 20px;
display: grid;
grid-template-columns: 200px 1fr;
row-gap: 12px;
column-gap: 10px;
}
.collapsed {
height: 0 !important;
}
label {
font-weight: 600;
}
input {
padding: 6px 8px;
border: 1px solid #ccc;
border-radius: 5px;
width: 230px;
}
button {
margin-top: 25px;
padding: 12px 28px;
font-size: 16px;
background: #4a8df8;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
}
button:hover {
background: #3b78d4;
}
/* 错误样式 */
.input-error {
border: 2px solid #ff5b5b !important;
background: #ffecec;
}
.error-text {
color: #d9534f;
font-size: 12px;
grid-column: 1 / span 2;
}
/* Toast 提示 */
#toast {
position: fixed;
top: 20px;
right: 20px;
background: #4CAF50;
color: white;
padding: 12px 18px;
border-radius: 6px;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2);
opacity: 0;
pointer-events: none;
transition: opacity 0.3s, transform 0.3s;
transform: translateY(-10px);
font-size: 14px;
z-index: 999;
}
#toast.show {
opacity: 1;
transform: translateY(0);
}
</style>
</head>
<body>
<h2>设备配置界面</h2>
<div id="toast"></div>
<div id="form"></div>
<button onclick="saveConfig()">保存配置</button>
<script>
function showToast(msg, error = false) {
let t = document.getElementById("toast");
t.innerText = msg;
t.style.background = error ? "#d9534f" : "#4CAF50";
t.classList.add("show");
setTimeout(() => {
t.classList.remove("show");
}, 2000);
}
async function loadData() {
let resp = await fetch("/config/data");
let data = await resp.json();
let schema = data.schema;
let config = data.config;
window.schema_cache = schema;
window.title_map = {};
window.old_devno = config["vehicle"]["device_no"];
let html = "";
for (let section in schema) {
html += `
<div class="card">
<div class="card-header" onclick="toggleSection('${section}')">
[${section}]
<span id="${section}_arrow" class="arrow"></span>
</div>
<div id="${section}_content" class="card-body">
<div class="content-wrapper">
`;
for (let key in schema[section]) {
let item = schema[section][key];
let title = item.title;
let value = config[section][key];
window.title_map[title] = { section, key };
html += `
<label>${title}</label>
<input id="${section}_${key}" value="${value}">
`;
}
html += `
</div>
</div>
</div>
`;
}
document.getElementById("form").innerHTML = html;
}
function toggleSection(section) {
let outer = document.getElementById(section + "_content");
let inner = outer.querySelector(".content-wrapper");
let arrow = document.getElementById(section + "_arrow");
if (outer.classList.contains("collapsed")) {
outer.classList.remove("collapsed");
outer.style.height = "0px";
outer.offsetHeight;
outer.style.height = inner.scrollHeight + "px";
arrow.textContent = "▼";
outer.addEventListener("transitionend", function done() {
outer.style.height = "auto";
outer.removeEventListener("transitionend", done);
});
} else {
outer.style.height = inner.scrollHeight + "px";
outer.offsetHeight;
outer.style.height = "0";
outer.classList.add("collapsed");
arrow.textContent = "▶";
}
}
// ---------------------------
// 直接跳转到新域名
// ---------------------------
function jumpToNewDomain(newDevNo) {
let url = `http://${newDevNo}.local:18080/config`;
showToast(`设备编号已更新,跳转到 ${newDevNo}.local ...`);
setTimeout(() => {
window.location.href = url;
}, 500);
}
async function saveConfig() {
let schema = window.schema_cache;
let new_config = {};
for (let section in schema) {
new_config[section] = {};
for (let key in schema[section]) {
new_config[section][key] = document.getElementById(`${section}_${key}`).value;
}
}
let resp = await fetch("/config/save", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(new_config)
});
let result = await resp.json();
if (result.status === "error") {
showToast(result.message, true);
let match = result.message.match(/字段 \[(.*)\]/);
if (match) {
let title = match[1];
let pos = window.title_map[title];
if (pos) {
let input = document.getElementById(`${pos.section}_${pos.key}`);
let outer = document.getElementById(pos.section + "_content");
let arrow = document.getElementById(pos.section + "_arrow");
outer.classList.remove("collapsed");
outer.style.height = "auto";
arrow.textContent = "▼";
document.querySelectorAll(".input-error").forEach(el => el.classList.remove("input-error"));
document.querySelectorAll(".error-text").forEach(el => el.remove());
input.classList.add("input-error");
let err = document.createElement("div");
err.className = "error-text";
err.innerText = result.message;
input.insertAdjacentElement("afterend", err);
input.scrollIntoView({ behavior: "smooth", block: "center" });
}
}
return;
}
showToast("配置已保存");
// -------- 跳转到新域名 --------
let newDevNo = new_config["vehicle"]["device_no"];
if (newDevNo && newDevNo !== window.old_devno) {
jumpToNewDomain(newDevNo);
}
}
loadData();
</script>
</body>
</html>

28
bin/config.json Normal file
View File

@ -0,0 +1,28 @@
{
"vehicle": {
"device_no": "KL001",
"vehicle_id": "V010001",
"vin": "LSV1234567890KUNL"
},
"cloud": {
"platform_ip": "36.137.175.196",
"platform_port": 11101,
"mqtt_ip": "192.168.4.196",
"mqtt_port": 1883,
"mqtt_username": "",
"mqtt_password": ""
},
"cockpit": {
"mqtt_ip": "192.168.1.110",
"mqtt_port": 1883
},
"serial": {
"dev_name": "/dev/ttyUSB3",
"baudrate": 115200
},
"tbox": {
"login_seq": 1,
"login_seq_date": "000000",
"heartbeat_interval": 30
}
}

92
bin/config_schema.json Normal file
View File

@ -0,0 +1,92 @@
{
"vehicle": {
"device_no": {
"type": "string",
"default": "KL001",
"title": "设备编号"
},
"vehicle_id": {
"type": "string",
"default": "V010001",
"title": "车辆编号"
},
"vin": {
"type": "string",
"default": "LSV1234567890KUNL",
"title": "车辆 VIN"
}
},
"cloud": {
"platform_ip": {
"type": "string",
"default": "36.137.175.196",
"title": "平台 IP"
},
"platform_port": {
"type": "int",
"default": 11101,
"title": "平台端口"
},
"mqtt_ip": {
"type": "string",
"default": "192.168.4.196",
"title": "MQTT 服务器 IP"
},
"mqtt_port": {
"type": "int",
"default": 1883,
"title": "MQTT 端口"
},
"mqtt_username": {
"type": "string",
"default": "",
"title": "MQTT 用户名"
},
"mqtt_password": {
"type": "string",
"default": "",
"title": "MQTT 密码"
}
},
"cockpit": {
"mqtt_ip": {
"type": "string",
"default": "192.168.1.110",
"title": "驾驶舱 MQTT IP"
},
"mqtt_port": {
"type": "int",
"default": 1883,
"title": "驾驶舱 MQTT 端口"
}
},
"serial": {
"dev_name": {
"type": "string",
"default": "/dev/ttyUSB3",
"title": "串口设备名"
},
"baudrate": {
"type": "int",
"default": 115200,
"title": "串口波特率"
}
},
"tbox": {
"login_seq": {
"type": "int",
"default": 1,
"title": "登入序号"
},
"login_seq_date": {
"type": "string",
"default": "000000",
"title": "登入日期"
},
"heartbeat_interval": {
"type": "int",
"default": 60,
"title": "心跳间隔(秒)"
}
}
}

BIN
bin/config_server Executable file

Binary file not shown.

12337
include/httplib.h Normal file

File diff suppressed because it is too large Load Diff

25526
include/json.hpp Normal file

File diff suppressed because it is too large Load Diff

187
include/json_fwd.hpp Normal file
View File

@ -0,0 +1,187 @@
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
#ifndef INCLUDE_NLOHMANN_JSON_FWD_HPP_
#define INCLUDE_NLOHMANN_JSON_FWD_HPP_
#include <cstdint> // int64_t, uint64_t
#include <map> // map
#include <memory> // allocator
#include <string> // string
#include <vector> // vector
// #include <nlohmann/detail/abi_macros.hpp>
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
// This file contains all macro definitions affecting or depending on the ABI
#ifndef JSON_SKIP_LIBRARY_VERSION_CHECK
#if defined(NLOHMANN_JSON_VERSION_MAJOR) && defined(NLOHMANN_JSON_VERSION_MINOR) && defined(NLOHMANN_JSON_VERSION_PATCH)
#if NLOHMANN_JSON_VERSION_MAJOR != 3 || NLOHMANN_JSON_VERSION_MINOR != 12 || NLOHMANN_JSON_VERSION_PATCH != 0
#warning "Already included a different version of the library!"
#endif
#endif
#endif
#define NLOHMANN_JSON_VERSION_MAJOR 3 // NOLINT(modernize-macro-to-enum)
#define NLOHMANN_JSON_VERSION_MINOR 12 // NOLINT(modernize-macro-to-enum)
#define NLOHMANN_JSON_VERSION_PATCH 0 // NOLINT(modernize-macro-to-enum)
#ifndef JSON_DIAGNOSTICS
#define JSON_DIAGNOSTICS 0
#endif
#ifndef JSON_DIAGNOSTIC_POSITIONS
#define JSON_DIAGNOSTIC_POSITIONS 0
#endif
#ifndef JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON
#define JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON 0
#endif
#if JSON_DIAGNOSTICS
#define NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS _diag
#else
#define NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS
#endif
#if JSON_DIAGNOSTIC_POSITIONS
#define NLOHMANN_JSON_ABI_TAG_DIAGNOSTIC_POSITIONS _dp
#else
#define NLOHMANN_JSON_ABI_TAG_DIAGNOSTIC_POSITIONS
#endif
#if JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON
#define NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON _ldvcmp
#else
#define NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON
#endif
#ifndef NLOHMANN_JSON_NAMESPACE_NO_VERSION
#define NLOHMANN_JSON_NAMESPACE_NO_VERSION 0
#endif
// Construct the namespace ABI tags component
#define NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c) json_abi ## a ## b ## c
#define NLOHMANN_JSON_ABI_TAGS_CONCAT(a, b, c) \
NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c)
#define NLOHMANN_JSON_ABI_TAGS \
NLOHMANN_JSON_ABI_TAGS_CONCAT( \
NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS, \
NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON, \
NLOHMANN_JSON_ABI_TAG_DIAGNOSTIC_POSITIONS)
// Construct the namespace version component
#define NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT_EX(major, minor, patch) \
_v ## major ## _ ## minor ## _ ## patch
#define NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT(major, minor, patch) \
NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT_EX(major, minor, patch)
#if NLOHMANN_JSON_NAMESPACE_NO_VERSION
#define NLOHMANN_JSON_NAMESPACE_VERSION
#else
#define NLOHMANN_JSON_NAMESPACE_VERSION \
NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT(NLOHMANN_JSON_VERSION_MAJOR, \
NLOHMANN_JSON_VERSION_MINOR, \
NLOHMANN_JSON_VERSION_PATCH)
#endif
// Combine namespace components
#define NLOHMANN_JSON_NAMESPACE_CONCAT_EX(a, b) a ## b
#define NLOHMANN_JSON_NAMESPACE_CONCAT(a, b) \
NLOHMANN_JSON_NAMESPACE_CONCAT_EX(a, b)
#ifndef NLOHMANN_JSON_NAMESPACE
#define NLOHMANN_JSON_NAMESPACE \
nlohmann::NLOHMANN_JSON_NAMESPACE_CONCAT( \
NLOHMANN_JSON_ABI_TAGS, \
NLOHMANN_JSON_NAMESPACE_VERSION)
#endif
#ifndef NLOHMANN_JSON_NAMESPACE_BEGIN
#define NLOHMANN_JSON_NAMESPACE_BEGIN \
namespace nlohmann \
{ \
inline namespace NLOHMANN_JSON_NAMESPACE_CONCAT( \
NLOHMANN_JSON_ABI_TAGS, \
NLOHMANN_JSON_NAMESPACE_VERSION) \
{
#endif
#ifndef NLOHMANN_JSON_NAMESPACE_END
#define NLOHMANN_JSON_NAMESPACE_END \
} /* namespace (inline namespace) NOLINT(readability/namespace) */ \
} // namespace nlohmann
#endif
/*!
@brief namespace for Niels Lohmann
@see https://github.com/nlohmann
@since version 1.0.0
*/
NLOHMANN_JSON_NAMESPACE_BEGIN
/*!
@brief default JSONSerializer template argument
This serializer ignores the template arguments and uses ADL
([argument-dependent lookup](https://en.cppreference.com/w/cpp/language/adl))
for serialization.
*/
template<typename T = void, typename SFINAE = void>
struct adl_serializer;
/// a class to store JSON values
/// @sa https://json.nlohmann.me/api/basic_json/
template<template<typename U, typename V, typename... Args> class ObjectType =
std::map,
template<typename U, typename... Args> class ArrayType = std::vector,
class StringType = std::string, class BooleanType = bool,
class NumberIntegerType = std::int64_t,
class NumberUnsignedType = std::uint64_t,
class NumberFloatType = double,
template<typename U> class AllocatorType = std::allocator,
template<typename T, typename SFINAE = void> class JSONSerializer =
adl_serializer,
class BinaryType = std::vector<std::uint8_t>, // cppcheck-suppress syntaxError
class CustomBaseClass = void>
class basic_json;
/// @brief JSON Pointer defines a string syntax for identifying a specific value within a JSON document
/// @sa https://json.nlohmann.me/api/json_pointer/
template<typename RefStringType>
class json_pointer;
/*!
@brief default specialization
@sa https://json.nlohmann.me/api/json/
*/
using json = basic_json<>;
/// @brief a minimal map-like container that preserves insertion order
/// @sa https://json.nlohmann.me/api/ordered_map/
template<class Key, class T, class IgnoredLess, class Allocator>
struct ordered_map;
/// @brief specialization that maintains the insertion order of object keys
/// @sa https://json.nlohmann.me/api/ordered_json/
using ordered_json = basic_json<nlohmann::ordered_map>;
NLOHMANN_JSON_NAMESPACE_END
#endif // INCLUDE_NLOHMANN_JSON_FWD_HPP_

Binary file not shown.

Binary file not shown.

Binary file not shown.

171
mdns/install_mdns.sh Executable file
View File

@ -0,0 +1,171 @@
#!/bin/bash
set -e
# --------------------------------------------
# 参数解析:必须指定 config.json 路径
# --------------------------------------------
CONFIG_PATH=""
while [[ $# -gt 0 ]]; do
case "$1" in
-c|--config)
CONFIG_PATH="$2"
shift 2
;;
*)
echo "❌ 未知参数: $1"
exit 1
;;
esac
done
if [[ -z "$CONFIG_PATH" ]]; then
echo "❌ 必须使用 -c <config.json 路径>"
echo "示例: sudo ./install_mdns.sh -c /home/aiec/demo/config.json"
exit 1
fi
if [[ ! -f "$CONFIG_PATH" ]]; then
echo "❌ 找不到配置文件:"
echo " $CONFIG_PATH"
exit 1
fi
echo "📌 使用配置文件: $CONFIG_PATH"
# --------------------------------------------
# 1. 安装 avahi 组件
# --------------------------------------------
echo "=== 安装 Avahi 相关 deb 包 ==="
sudo dpkg -i avahi-daemon_*.deb avahi-utils_*.deb avahi-autoipd_*.deb libnss-mdns_*.deb || true
sudo apt --fix-broken install -y
echo "=== 启动 & 启用 avahi-daemon ==="
sudo systemctl enable avahi-daemon || true
sudo systemctl restart avahi-daemon
# --------------------------------------------
# 2. 安装 update_mdns.sh核心逻辑
# --------------------------------------------
echo "=== 写入 /usr/local/bin/update_mdns.sh ==="
sudo mkdir -p /usr/local/bin
sudo tee /usr/local/bin/update_mdns.sh >/dev/null <<'EOF'
#!/bin/bash
CONFIG="@CONFIG_PATH@"
TRIGGER="/run/update_mdns_request"
# --------------- 读取 device_no ---------------
DEVICE_NO=$(grep -oP '"device_no"\s*:\s*"\K[^"]+' "$CONFIG")
# 若触发文件存在,则优先使用触发文件内容
if [[ -f "$TRIGGER" ]]; then
FILE_VALUE=$(tr -d '\n' < "$TRIGGER")
if [[ -n "$FILE_VALUE" ]]; then
DEVICE_NO="$FILE_VALUE"
fi
fi
if [[ -z "$DEVICE_NO" ]]; then
echo "[update_mdns] 未找到有效 device_no"
exit 1
fi
echo "[update_mdns] 更新 hostname = $DEVICE_NO"
# --------------- 设置 hostname ---------------
hostnamectl set-hostname "$DEVICE_NO"
# 修复 /etc/hosts
if grep -q "^127.0.1.1" /etc/hosts; then
sed -i "s/^127.0.1.1.*/127.0.1.1 $DEVICE_NO/" /etc/hosts
else
echo "127.0.1.1 $DEVICE_NO" >> /etc/hosts
fi
# --------------- 更新 Avahi 服务 ---------------
mkdir -p /etc/avahi/services
tee /etc/avahi/services/webconfig.service >/dev/null <<EOF2
<?xml version="1.0" standalone='no'?>
<!DOCTYPE service-group SYSTEM "avahi-service.dtd">
<service-group>
<name replace-wildcards="yes">%h Config Page</name>
<service>
<type>_http._tcp</type>
<port>18080</port>
</service>
</service-group>
EOF2
systemctl restart avahi-daemon
# 删除触发文件,避免重复触发
rm -f "$TRIGGER"
EOF
sudo chmod +x /usr/local/bin/update_mdns.sh
# 用真实 CONFIG_PATH 替换脚本中的占位符
sudo sed -i "s|@CONFIG_PATH@|$CONFIG_PATH|g" /usr/local/bin/update_mdns.sh
# --------------------------------------------
# 3. 安装 systemd service
# --------------------------------------------
echo "=== 创建 update-mdns.service ==="
sudo tee /etc/systemd/system/update-mdns.service >/dev/null <<EOF
[Unit]
Description=Update hostname & mDNS when device_no changed
[Service]
Type=oneshot
ExecStart=/usr/local/bin/update_mdns.sh
EOF
# --------------------------------------------
# 4. 安装 systemd path watcher
# --------------------------------------------
echo "=== 创建 update-mdns.path ==="
sudo tee /etc/systemd/system/update-mdns.path >/dev/null <<EOF
[Unit]
Description=Watch for mDNS update request
[Path]
PathExists=/run/update_mdns_request
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable update-mdns.path
sudo systemctl start update-mdns.path
echo ""
echo "============================================="
echo "mDNS 自动更新机制安装完成!🎉"
echo "配置文件路径:$CONFIG_PATH"
echo ""
echo "当 C++ 程序写入: /run/update_mdns_request"
echo "内容 = 新设备号,例如: KL777"
echo "系统会立即更新:"
echo " • hostname"
echo " • /etc/hosts"
echo " • avahi 广播名字xxx.local"
echo "============================================="

Binary file not shown.

256
src/config_server.cpp Normal file
View File

@ -0,0 +1,256 @@
#include <iostream>
#include <fstream>
#include <sstream>
#include <optional>
#include "httplib.h"
#include "json.hpp"
using ordered_json = nlohmann::ordered_json; // 关键:保留 JSON 字段顺序
ordered_json schema;
ordered_json config;
// ---------------------------
// 读取文件为字符串
// ---------------------------
std::string read_file(const std::string &path)
{
std::ifstream ifs(path);
if (!ifs.is_open())
return "";
std::stringstream ss;
ss << ifs.rdbuf();
return ss.str();
}
// ---------------------------
// 写文件
// ---------------------------
void write_file(const std::string &path, const std::string &data)
{
std::ofstream ofs(path);
ofs << data;
}
// ---------------------------
// 加载 schema.json
// ---------------------------
void load_schema()
{
std::string txt = read_file("config_schema.json");
if (txt.empty())
{
std::cerr << "ERROR: config_schema.json not found!\n";
exit(1);
}
schema = ordered_json::parse(txt);
}
// ---------------------------
// 加载 config.json如果不存在则创建
// ---------------------------
void load_or_create_config()
{
std::string txt = read_file("config.json");
if (txt.empty())
{
std::cout << "config.json not found, creating default.\n";
// 自动根据 schema 生成一份初始配置
for (auto &[section, items] : schema.items())
{
for (auto &[key, meta] : items.items())
{
config[section][key] = meta["default"];
}
}
write_file("config.json", config.dump(4));
return;
}
try
{
config = ordered_json::parse(txt);
}
catch (...)
{
std::cerr << "config.json corrupted, recreating.\n";
config.clear();
}
// 自动修复(补全缺失字段)
for (auto &[section, items] : schema.items())
{
for (auto &[key, meta] : items.items())
{
if (!config[section].contains(key))
{
config[section][key] = meta["default"];
}
}
}
write_file("config.json", config.dump(4));
}
// ---------------------------
// 保存配置(含自动类型校验)
// ---------------------------
std::optional<std::string> validate_config(
const ordered_json &incoming, ordered_json &out)
{
for (auto &[section, items] : schema.items())
{
if (!incoming.contains(section))
return "缺少配置段:" + section;
out[section] = ordered_json::object();
for (auto &[key, meta] : items.items())
{
if (!incoming[section].contains(key))
return "字段 [" + meta.value("title", key) + "] 缺失";
auto &value = incoming[section][key];
if (value.is_null())
return "字段 [" + meta.value("title", key) + "] 不能为空";
std::string type = meta["type"];
std::string title = meta.value("title", key);
try
{
if (type == "int")
{
if (value.is_number_integer())
{
out[section][key] = value.get<int>();
}
else if (value.is_string())
{
out[section][key] = std::stoi(value.get<std::string>());
}
else
{
return "字段 [" + title + "] 必须为整数";
}
}
else if (type == "bool")
{
if (value.is_boolean())
{
out[section][key] = value.get<bool>();
}
else if (value.is_string())
{
std::string v = value.get<std::string>();
if (v == "1" || v == "true")
out[section][key] = true;
else if (v == "0" || v == "false")
out[section][key] = false;
else
return "字段 [" + title + "] 必须为布尔值";
}
else
{
return "字段 [" + title + "] 必须为布尔值";
}
}
else
{ // string
if (!value.is_string())
return "字段 [" + title + "] 必须为字符串";
out[section][key] = value.get<std::string>();
}
}
catch (...)
{
return "字段 [" + title + "] 格式无效";
}
}
}
return std::nullopt;
}
int main()
{
load_schema();
load_or_create_config();
httplib::Server svr;
// 返回页面 HTML
svr.Get("/config", [](const httplib::Request &, httplib::Response &res)
{ res.set_content(read_file("config.html"), "text/html"); });
// 返回 schema + config
svr.Get("/config/data", [](const httplib::Request &, httplib::Response &res)
{
// ---- 动态加载 schema.json (热更新)----
ordered_json fresh_schema;
try {
fresh_schema = ordered_json::parse(read_file("config_schema.json"));
} catch(...) {
// schema 文件损坏或读取失败
fresh_schema = schema; // fallback 用旧 schema
}
// ---- 自动修复 config补齐 schema 里新增的字段 ----
for (auto &[section, items] : fresh_schema.items())
{
if (!config.contains(section))
config[section] = ordered_json::object();
for (auto &[key, meta] : items.items())
{
if (!config[section].contains(key))
config[section][key] = meta["default"];
}
}
// ---- 覆盖全局 schema ----
schema = fresh_schema;
// ---- 返回 schema + config ----
ordered_json out;
out["schema"] = schema;
out["config"] = config;
res.set_content(out.dump(), "application/json");
});
// 保存配置
svr.Post("/config/save", [](const httplib::Request &req, httplib::Response &res)
{
ordered_json incoming = ordered_json::parse(req.body);
ordered_json validated;
auto err = validate_config(incoming, validated);
if (err.has_value()) {
ordered_json r;
r["status"] = "error";
r["message"] = err.value();
res.set_content(r.dump(), "application/json");
return;
}
config = validated;
write_file("config.json", config.dump(4));
// ★★★ 写入 mDNS 更新请求 ★★★
std::string new_devno = config["vehicle"]["device_no"];
write_file("/run/update_mdns_request", new_devno);
res.set_content("{\"status\":\"ok\"}", "application/json"); });
std::cout << "Config server running at http://0.0.0.0:18080/config\n";
svr.listen("0.0.0.0", 18080);
return 0;
}