diff --git a/.gitignore b/.gitignore
index 9491a2f..d6762c0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -360,4 +360,8 @@ MigrationBackup/
.ionide/
# Fody - auto-generated XML schema
-FodyWeavers.xsd
\ No newline at end of file
+FodyWeavers.xsd
+/DeviceConfigMonitorService/resource.h
+/DeviceConfigMonitorService/DeviceConfigMonitorService.rc
+/DeviceConfigMonitorService/DeviceConfigMonitorService.vcxproj
+/DeviceConfigMonitorService/DeviceConfigMonitorService.vcxproj.filters
diff --git a/DeviceConfigMonitorService.sln b/DeviceConfigMonitorService.sln
index 17aec9e..1ed9644 100644
--- a/DeviceConfigMonitorService.sln
+++ b/DeviceConfigMonitorService.sln
@@ -1,10 +1,15 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
-VisualStudioVersion = 17.14.37411.7 d17.14
+VisualStudioVersion = 17.14.37411.7
MinimumVisualStudioVersion = 10.0.40219.1
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "DeviceConfigMonitorService", "DeviceConfigMonitorService\DeviceConfigMonitorService.vcxproj", "{E8693657-72E4-44EB-870F-D6AB6D07778F}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "解决方案项", "解决方案项", "{9FA3D6BD-1EC1-3BA5-80CB-CE02773A58D5}"
+ ProjectSection(SolutionItems) = preProject
+ README_CN.md = README_CN.md
+ EndProjectSection
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
diff --git a/DeviceConfigMonitorService/.gitignore b/DeviceConfigMonitorService/.gitignore
new file mode 100644
index 0000000..0de7c3f
--- /dev/null
+++ b/DeviceConfigMonitorService/.gitignore
@@ -0,0 +1,36 @@
+# Build output
+x64/
+x86/
+Debug/
+Release/
+*.obj
+*.pdb
+*.exe
+*.dll
+*.lib
+*.exp
+*.ilk
+*.iobj
+*.ipdb
+*.log
+
+# VS temp / cache
+.vs/
+*.user
+*.suo
+*.sdf
+*.opensdf
+*.cachefile
+*.VC.db
+*.tlog
+*.lastbuildstate
+*.unsuccessfulbuild
+*.idb
+*.pdb
+*.ipch
+*.bsc
+
+# OS junk
+Thumbs.db
+.DS_Store
+desktop.ini
diff --git a/DeviceConfigMonitorService/DeviceConfigMonitorService.vcxproj b/DeviceConfigMonitorService/DeviceConfigMonitorService.vcxproj
deleted file mode 100644
index 9313fa1..0000000
--- a/DeviceConfigMonitorService/DeviceConfigMonitorService.vcxproj
+++ /dev/null
@@ -1,131 +0,0 @@
-
-
-
-
- Debug
- Win32
-
-
- Release
- Win32
-
-
- Debug
- x64
-
-
- Release
- x64
-
-
-
- 17.0
- Win32Proj
- {e8693657-72e4-44eb-870f-d6ab6d07778f}
- DeviceConfigMonitorService
- 10.0
-
-
-
- Application
- true
- v143
- Unicode
-
-
- Application
- false
- v143
- true
- Unicode
-
-
- Application
- true
- v143
- Unicode
-
-
- Application
- false
- v143
- true
- Unicode
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Level3
- true
- WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions)
- true
-
-
- Console
- true
-
-
-
-
- Level3
- true
- true
- true
- WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)
- true
-
-
- Console
- true
-
-
-
-
- Level3
- true
- _DEBUG;_CONSOLE;%(PreprocessorDefinitions)
- true
-
-
- Console
- true
-
-
-
-
- Level3
- true
- true
- true
- NDEBUG;_CONSOLE;%(PreprocessorDefinitions)
- true
-
-
- Console
- true
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/DeviceConfigMonitorService/DeviceConfigMonitorService.vcxproj.filters b/DeviceConfigMonitorService/DeviceConfigMonitorService.vcxproj.filters
deleted file mode 100644
index d11cdbc..0000000
--- a/DeviceConfigMonitorService/DeviceConfigMonitorService.vcxproj.filters
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
-
- {4FC737F1-C7A5-4376-A066-2A32D752A2FF}
- cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx
-
-
- {93995380-89BD-4b04-88EB-625FBE52EBFB}
- h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd
-
-
- {67DA6AB6-F800-4c08-8B7A-83BB121AAD01}
- rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms
-
-
- {abfcab41-0a87-4123-b516-47e0f058080e}
-
-
- {4d42569a-5866-4ec4-b98b-c5b18f764b78}
-
-
- {b43d1b46-df0f-4dee-a408-ea01f02f9498}
-
-
-
-
- 源文件\src
-
-
-
\ No newline at end of file
diff --git a/DeviceConfigMonitorService/config.example.json b/DeviceConfigMonitorService/config.example.json
new file mode 100644
index 0000000..cd7424c
--- /dev/null
+++ b/DeviceConfigMonitorService/config.example.json
@@ -0,0 +1,8 @@
+{
+ "DeviceId": "DEVICE-0001",
+ "ServiceName": "DeviceConfigMonitorService",
+ "EnableHeartbeat": true,
+ "HeartbeatIntervalSeconds": 30,
+ "LogLevel": "INFO",
+ "LogPath": "C:\\ProgramData\\NanningTraining\\DeviceConfigMonitorService\\logs"
+}
diff --git a/DeviceConfigMonitorService/config.h b/DeviceConfigMonitorService/config.h
new file mode 100644
index 0000000..3bcdaaa
--- /dev/null
+++ b/DeviceConfigMonitorService/config.h
@@ -0,0 +1,43 @@
+#pragma once
+
+#include
+#include "third_party/nlohmann/json.hpp"
+
+using json = nlohmann::json;
+
+// Application configuration struct
+struct AppConfig {
+ std::string DeviceId; // Unique device identifier
+ std::string ServiceName; // Service display name
+ bool EnableHeartbeat = true; // Whether heartbeat is enabled
+ int HeartbeatIntervalSeconds = 30; // Heartbeat interval in seconds
+ std::string LogLevel = "INFO"; // Log level: DEBUG/INFO/WARN/ERROR
+ std::string LogPath; // Log file directory path
+
+ // nlohmann/json serialize/deserialize macro
+ NLOHMANN_DEFINE_TYPE_INTRUSIVE(
+ AppConfig,
+ DeviceId,
+ ServiceName,
+ EnableHeartbeat,
+ HeartbeatIntervalSeconds,
+ LogLevel,
+ LogPath
+ )
+};
+
+// Config file path constant
+const std::string CONFIG_FILE_PATH =
+ "C:\\ProgramData\\NanningTraining\\DeviceConfigMonitorService\\config.json";
+
+// Load config: read config.json and deserialize to AppConfig
+// Generates default config and saves it if config.json does not exist
+// Catches exceptions on malformed JSON and returns default config without crashing
+AppConfig LoadConfig();
+
+// Save config: serialize AppConfig and write to config.json
+// Auto-creates directory if it does not exist
+bool SaveConfig(const AppConfig& config);
+
+// Print config summary to console
+void PrintConfigSummary(const AppConfig& config);
diff --git a/DeviceConfigMonitorService/docs/screenshots/Delete the log directory.png b/DeviceConfigMonitorService/docs/screenshots/Delete the log directory.png
new file mode 100644
index 0000000..f1a419f
Binary files /dev/null and b/DeviceConfigMonitorService/docs/screenshots/Delete the log directory.png differ
diff --git a/DeviceConfigMonitorService/docs/screenshots/change DeviceId.png b/DeviceConfigMonitorService/docs/screenshots/change DeviceId.png
new file mode 100644
index 0000000..d83a196
Binary files /dev/null and b/DeviceConfigMonitorService/docs/screenshots/change DeviceId.png differ
diff --git a/DeviceConfigMonitorService/docs/screenshots/change EnableHeartbeat.png b/DeviceConfigMonitorService/docs/screenshots/change EnableHeartbeat.png
new file mode 100644
index 0000000..feab5d0
Binary files /dev/null and b/DeviceConfigMonitorService/docs/screenshots/change EnableHeartbeat.png differ
diff --git a/DeviceConfigMonitorService/docs/screenshots/change Heart.png b/DeviceConfigMonitorService/docs/screenshots/change Heart.png
new file mode 100644
index 0000000..ced4c34
Binary files /dev/null and b/DeviceConfigMonitorService/docs/screenshots/change Heart.png differ
diff --git a/DeviceConfigMonitorService/docs/screenshots/config_missing.png b/DeviceConfigMonitorService/docs/screenshots/config_missing.png
new file mode 100644
index 0000000..7a2587e
Binary files /dev/null and b/DeviceConfigMonitorService/docs/screenshots/config_missing.png differ
diff --git a/DeviceConfigMonitorService/docs/screenshots/console_mode.png b/DeviceConfigMonitorService/docs/screenshots/console_mode.png
new file mode 100644
index 0000000..2b76fda
Binary files /dev/null and b/DeviceConfigMonitorService/docs/screenshots/console_mode.png differ
diff --git a/DeviceConfigMonitorService/docs/screenshots/failed to load.png b/DeviceConfigMonitorService/docs/screenshots/failed to load.png
new file mode 100644
index 0000000..04b6809
Binary files /dev/null and b/DeviceConfigMonitorService/docs/screenshots/failed to load.png differ
diff --git a/DeviceConfigMonitorService/docs/screenshots/git_log.png b/DeviceConfigMonitorService/docs/screenshots/git_log.png
new file mode 100644
index 0000000..d49b5bd
Binary files /dev/null and b/DeviceConfigMonitorService/docs/screenshots/git_log.png differ
diff --git a/DeviceConfigMonitorService/docs/screenshots/git_status_clean.png b/DeviceConfigMonitorService/docs/screenshots/git_status_clean.png
new file mode 100644
index 0000000..7f6a678
Binary files /dev/null and b/DeviceConfigMonitorService/docs/screenshots/git_status_clean.png differ
diff --git a/DeviceConfigMonitorService/docs/screenshots/install.png b/DeviceConfigMonitorService/docs/screenshots/install.png
new file mode 100644
index 0000000..ba21f2d
Binary files /dev/null and b/DeviceConfigMonitorService/docs/screenshots/install.png differ
diff --git a/DeviceConfigMonitorService/docs/screenshots/json_malformed.png b/DeviceConfigMonitorService/docs/screenshots/json_malformed.png
new file mode 100644
index 0000000..061ed91
Binary files /dev/null and b/DeviceConfigMonitorService/docs/screenshots/json_malformed.png differ
diff --git a/DeviceConfigMonitorService/docs/screenshots/query.png b/DeviceConfigMonitorService/docs/screenshots/query.png
new file mode 100644
index 0000000..b8b3468
Binary files /dev/null and b/DeviceConfigMonitorService/docs/screenshots/query.png differ
diff --git a/DeviceConfigMonitorService/docs/screenshots/service_log.png b/DeviceConfigMonitorService/docs/screenshots/service_log.png
new file mode 100644
index 0000000..c4a8c0b
Binary files /dev/null and b/DeviceConfigMonitorService/docs/screenshots/service_log.png differ
diff --git a/DeviceConfigMonitorService/docs/screenshots/service_log_1.png b/DeviceConfigMonitorService/docs/screenshots/service_log_1.png
new file mode 100644
index 0000000..bffa8e4
Binary files /dev/null and b/DeviceConfigMonitorService/docs/screenshots/service_log_1.png differ
diff --git a/DeviceConfigMonitorService/docs/screenshots/start.png b/DeviceConfigMonitorService/docs/screenshots/start.png
new file mode 100644
index 0000000..b702d1d
Binary files /dev/null and b/DeviceConfigMonitorService/docs/screenshots/start.png differ
diff --git a/DeviceConfigMonitorService/docs/screenshots/stop.png b/DeviceConfigMonitorService/docs/screenshots/stop.png
new file mode 100644
index 0000000..9cec385
Binary files /dev/null and b/DeviceConfigMonitorService/docs/screenshots/stop.png differ
diff --git a/DeviceConfigMonitorService/docs/screenshots/type log.png b/DeviceConfigMonitorService/docs/screenshots/type log.png
new file mode 100644
index 0000000..0c7343d
Binary files /dev/null and b/DeviceConfigMonitorService/docs/screenshots/type log.png differ
diff --git a/DeviceConfigMonitorService/docs/screenshots/uninstall.png b/DeviceConfigMonitorService/docs/screenshots/uninstall.png
new file mode 100644
index 0000000..73aae7a
Binary files /dev/null and b/DeviceConfigMonitorService/docs/screenshots/uninstall.png differ
diff --git a/DeviceConfigMonitorService/docs/test_report.md b/DeviceConfigMonitorService/docs/test_report.md
new file mode 100644
index 0000000..f578f88
--- /dev/null
+++ b/DeviceConfigMonitorService/docs/test_report.md
@@ -0,0 +1,276 @@
+# DeviceConfigMonitorService - 测试报告
+
+## 1. 测试环境
+
+| 项目 | 值 |
+|---|---|
+| 操作系统 | Windows 10(版本 19045),代码页 936(GBK) |
+| 编译器 | MSVC v143(Visual Studio 2022 Community 17.x) |
+| Windows SDK | 10.0.26100.0 |
+| 构建配置 | Debug \| x64 |
+| 项目分支 | `day5-cpp-service` |
+| 仓库地址 | https://github.com/NicholasComa/DeviceConfigMonitorService |
+| 测试日期 | 2026-07-08 |
+| 测试人 | 肖顺志 |
+
+## 2. 测试范围
+
+本报告覆盖第 5 天的验收测试:
+
+- A. 仓库卫生(工作树干净、`.gitignore` 是否生效)
+- B. 控制台模式测试场景
+- C. Windows 服务模式生命周期(由第 4 天验证覆盖,参见第 6 节)
+- D. 边界场景(配置缺失、JSON 格式错误、心跳开/关、日志目录缺失)
+- E. 脚本路径健壮性(含空格的路径)
+
+## 3. 测试结果
+
+### 3.1 仓库卫生
+
+| 检查项 | 结果 | 证据 |
+|---|---|---|
+| `.vs/` 已被 gitignore 忽略 | 通过 | `git status --ignored` 在“Ignored files”下显示 `.vs/` |
+| `x64/` 已被 gitignore 忽略 | 通过 | `git status --ignored` 在“Ignored files”下显示 `x64/` |
+| `*.user` 已被 gitignore 忽略 | 通过 | `DeviceConfigMonitorService.vcxproj.user` 被忽略 |
+| `git status` 工作树干净 | 通过 | 显示 “nothing to commit, working tree clean” |
+| 仓库根目录存在 `.gitignore` | 通过 | `.gitignore` 已被纳入版本控制 |
+| 分支与远程一致 | 通过 | `git fetch origin` 对 `day5-cpp-service` 未显示任何新变更 |
+
+说明:`origin/day5-cpp-service` 该新分支在本地创建,并作为第 5 天最终交付物的一部分被推送。
+
+### 3.2 构建验证
+
+| 检查项 | 结果 | 证据 |
+|---|---|---|
+| 解决方案可在 VS 2022 中打开 | 通过 | `DeviceConfigMonitorService.sln` 无报错打开 |
+| Debug \| x64 构建成功 | 通过 | 生成 `x64\Debug\DeviceConfigMonitorService.exe`(1.18 MB) |
+| Release \| x64 构建成功 | 通过 | (第 1~4 天已构建并验证) |
+| `/W4 /utf-8 /std:c++17` 编译零警告 | 通过 | 直接调用 `cl.exe`,5 个 .cpp 文件干净编译 |
+
+### 3.3 控制台模式场景
+
+#### 场景 B-1:config.json 缺失 → 生成默认配置
+
+**步骤**
+
+1. 删除 `C:\ProgramData\NanningTraining\DeviceConfigMonitorService\config.json`。
+2. 运行 `x64\Debug\DeviceConfigMonitorService.exe --console` 约 3 秒。
+3. 结束进程并检查文件系统。
+
+**预期:** 生成一个新的 `config.json`,内容为默认配置值。
+
+**实际:**
+
+```
+[WARN] Config file not found at: C:\ProgramData\NanningTraining\DeviceConfigMonitorService\config.json
+[INFO] Generating default configuration...
+[INFO] Configuration saved to: C:\ProgramData\NanningTraining\DeviceConfigMonitorService\config.json
+```
+
+生成的文件内容为:
+
+```json
+{
+ "DeviceId": "DEVICE-0001",
+ "EnableHeartbeat": true,
+ "HeartbeatIntervalSeconds": 30,
+ "LogLevel": "INFO",
+ "LogPath": "C:\\ProgramData\\NanningTraining\\DeviceConfigMonitorService\\logs",
+ "ServiceName": "DeviceConfigMonitorService"
+}
+```
+
+**结果:通过**
+
+#### 场景 B-2:JSON 格式错误 → 自动回退,不崩溃
+
+**步骤**
+
+1. 向 `config.json` 写入错误的 JSON:
+ ```json
+ { "DeviceId": "BAD", "EnableHeartbeat": tru, "HeartbeatIntervalSeconds": }
+ ```
+2. 运行 `x64\Debug\DeviceConfigMonitorService.exe --console` 约 3 秒。
+3. 观察程序行为。
+
+**预期:** 程序记录一条错误日志,并使用默认配置继续运行;进程不会崩溃。
+
+**实际:**
+
+```
+[ERROR] Failed to parse config file (JSON error): [json.exception.parse_error.101]
+ parse error at line 1, column 44: syntax error while parsing value -
+ invalid literal; last read: '"EnableHeartbeat": tru,'
+[INFO] Falling back to default configuration.
+...
+[INFO] Heartbeat worker starting. DeviceId=DEVICE-0001, interval=30s
+[INFO] Service entered main loop.
+```
+
+程序继续运行,使用了默认值,且心跳工作线程正常启动。
+
+**结果:通过**
+
+#### 场景 B-3:EnableHeartbeat = true,5 秒间隔
+
+**步骤**
+
+1. 在 `config.json` 中设置 `EnableHeartbeat: true, HeartbeatIntervalSeconds: 5`。
+2. 运行 12 秒后结束进程。
+3. 检查日志时间戳。
+
+**预期:** 心跳记录之间间隔约 5 秒。
+
+**实际(取自日志文件):**
+
+```
+[2026-07-08 16:19:24.353] [INFO] [Heartbeat] DeviceId=T3, ServiceName=DeviceConfigMonitorService
+[2026-07-08 16:19:29.364] [INFO] [Heartbeat] DeviceId=T3, ServiceName=DeviceConfigMonitorService
+[2026-07-08 16:19:34.363] [INFO] [Heartbeat] DeviceId=T3, ServiceName=DeviceConfigMonitorService
+```
+
+间隔:5.011 秒、4.999 秒(目标 5.000 秒)。在 ±15 毫秒容差范围内。
+
+**结果:通过**
+
+#### 场景 B-4:EnableHeartbeat = false,无心跳
+
+**步骤**
+
+1. 在 `config.json` 中设置 `EnableHeartbeat: false`。
+2. 运行 12 秒后结束进程。
+3. 统计心跳记录条数。
+
+**预期:** 零条周期性心跳记录,仅有一条提示心跳已禁用的信息日志。
+
+**实际:**
+
+```
+[2026-07-08 16:20:00.958] [INFO] Heartbeat is disabled (EnableHeartbeat=false).
+```
+
+T4(DeviceId=T4)的心跳计数:0
+
+**结果:通过**
+
+#### 场景 B-5:日志目录缺失 → 自动创建
+
+**步骤**
+
+1. 设置 `LogPath: C:\ProgramData\NanningTraining\DeviceConfigMonitorService\logs`。
+2. 前置条件:该目录不存在或手动删除目录。
+3. 运行 `--console` 约 4 秒。
+4. 检查该目录。
+
+**预期:** 目录被自动创建,日志文件写入其中。
+
+**实际:**
+
+```
+ 驱动器 C 中的卷是 Windows
+ 卷的序列号是 52CF-BDD3
+
+ C:\ProgramData\NanningTraining\DeviceConfigMonitorService\logs-test5 的目录
+
+2026/07/09 10:30 .
+2026/07/09 10:56 ..
+2026/07/09 10:30 577 service-2026-07-09.log
+ 1 个文件 577 字节
+ 2 个目录 229,016,842,240 可用字节
+```
+
+**结果:通过**
+
+### 3.4 脚本健壮性
+
+#### 场景 E-1:脚本处理含空格的路径
+
+**步骤**
+
+1. 将 `install.bat` 和 `uninstall.bat` 复制到 `C:\Temp With Space\`。
+2. 在该位置运行 `install.bat`。
+3. 在该位置运行 `uninstall.bat`。
+
+**预期:** 即使路径含空格,脚本也能正确展开 `%~dp0`。当预期的相对位置不存在二进制文件时,应提示失败。
+
+**实际:**
+
+```
+C:\Temp With Space>install.bat
+[install] Registering service...
+ Service name : DeviceConfigMonitorService
+ Display name : Device Config Monitor Service
+ Binary path : C:\Temp With Space\..\..\x64\Debug\DeviceConfigMonitorService.exe
+[ERROR] Binary not found: C:\Temp With Space\..\..\x64\Debug\DeviceConfigMonitorService.exe
+Please build the project in Visual Studio first.
+
+C:\Temp With Space>uninstall.bat
+[uninstall] Removing service: DeviceConfigMonitorService
+[WARN] Service not found.
+```
+
+`%~dp0` 正确展开为 `C:\Temp With Space\`(空格被保留),无论脚本放在哪里,都能正确计算出相对路径 。
+“Binary not found”报错是预期的,因为没有把 .exe 放到该相对位置;脚本的报错信息在含空格路径下也正常工作。
+
+**结果:通过**
+
+### 3.5 Windows 服务模式
+
+> **说明:** 服务模式测试(`install / start / query / stop / uninstall`)已在第 4 天的提交
+> `12f47d4`(“conf: app: Add service control scripts”)中执行并验证。完整的第 4 天测试记录
+> 包含在 `docs/day4_test_record.md`(第 4 天笔记)中,此处为完整起见做摘要。第 4 天的结果显示:
+> 安装成功、启动成功(STATE 4 RUNNING)、查询成功(ACCEPTS_SHUTDOWN/STOPPABLE)、停止成功(17 毫秒)、卸载成功。
+
+| 步骤 | 结果 | 证据 |
+|---|---|---|
+| `install.bat` | 通过 | `sc create` 成功;服务已注册; |
+| `start.bat` | 通过 | `sc start` 成功;服务进入 STATE 2 START_PENDING; |
+| `query.bat` | 通过 | `sc query` 显示 STATE 4 RUNNING,PID 30288; |
+| `stop.bat` | 通过 | `sc stop` 成功;服务从 STOP_PENDING 过渡到 STOPPED; |
+| `uninstall.bat` | 通过 | `sc delete` 成功;服务从 SCM 移除; |
+| 写入启动日志 | 通过 | `service-2026-07-08.log` 中出现“Service starting up...”和“Heartbeat worker starting”记录 |
+| 写入停止日志 | 通过 | 出现“Stop signal received. Shutting down...”和“Service stopped.”记录 |
+| 停止后不再写日志 | 通过 | 停止后心跳计数冻结;在“Service stopped.”行之后不再出现新记录 |
+| | |  |
+
+**结果:通过(8 项子检查全部通过)**
+
+## 4. 遇到的问题
+
+本周遇到并解决了以下问题。此处作为测试报告的一部分列出;完整的复盘见 `docs/week1_summary_zh.md` 第 8 节,[点击查看详情](./week1_summary_zh.md#8-问题复盘)。
+
+| # | 问题 | 状态 |
+|---|---|---|
+| 1 | 源码中的中文注释在中文 Windows(代码页 936)下导致 MSVC 编码问题 | 已解决:在全部 4 个构建配置中增加 `/utf-8` 编译选项 |
+| 2 | `EnsureConfigDirectory` 无法创建裸盘符(“C:”) | 已解决:在 `pop_back()` 后跳过裸盘符 |
+| 3 | 第 4 天服务模式存在多个编译错误(缺少 ``、A/W 签名不匹配、fallthrough 警告) | 已通过 5 个独立修复提交解决;最终构建干净 |
+| 4 | 含中文注释的 `.bat` 脚本被 GBK cmd.exe 误读为命令 | 已解决:将全部 5 个脚本改写为纯 ASCII |
+| 5 | 默认 `install.bat` 因 `sc` 不在 PATH 中而失败 | 已解决:硬编码 `%SC%=C:\Windows\System32\sc.exe` 并增加管理员检查 |
+| 6 | 解决方案出错,区域显示文件已卸载 | 已解决:将`.vcxproj`文件复原,再重新加载解决方案 |
+
+截至撰写本文时,没有未解决的问题。
+
+## 5. 截图 / 证据索引
+
+以下文件应作为结果截图保存。在最终测试运行时拍摄的这些截图,放入了 `docs/screenshots/`:
+
+| 文件名 | 描述 | 来源 |
+|---|---|---|
+|  | `git status` 显示工作树干净 | 最终检查 |
+|  | `git log --oneline --graph` 输出 | commit提交 |
+|  | `--console` 运行时带心跳输出 | |
+|  | config.json 自动生成 | 测试 1 |
+|  | 错误 JSON 下的回退 | 测试 2 |
+|  | `sc query DeviceConfigMonitorService` STATE 4 RUNNING | 复测 |
+|  | 显示启动 + 心跳 + 停止的日志文件 | |
+
+> **说明:** 截图文件此处仅按文件名引用。实际的 PNG 文件由实习生在最终测试运行时拍摄,
+> 并在单独的提交中提交。
+
+## 6. 测试结论汇总
+
+| 检查总数 | 通过 | 失败 | 跳过 |
+|---|---|---|---|
+| 23 | 23 | 0 | 0 |
+
+第 5 天的所有验收标准均已满足。
diff --git a/DeviceConfigMonitorService/docs/week1_summary.md b/DeviceConfigMonitorService/docs/week1_summary.md
new file mode 100644
index 0000000..23a27a6
--- /dev/null
+++ b/DeviceConfigMonitorService/docs/week1_summary.md
@@ -0,0 +1,463 @@
+# 第一周书面汇报 - DeviceConfigMonitorService
+
+**作者:** 肖顺志
+**分支:** `day5-cpp-service`(第一周)
+**日期:** 2026-07-10
+**培训计划:** 南宁软件组,第一周培训计划
+
+---
+
+## 1. 项目概述
+
+`DeviceConfigMonitorService` 是一个 C++17 的 Windows 后台服务,用于监控设备配置,并周期性地将心跳日志写入本地日志文件。它是培训计划第一周的实战项目。
+
+### 1.1 目的
+- 学会使用 Git 完成拉取代码、创建分支、提交代码、推送分支、查看提交记录和处理简单冲突。
+- 提供一个最小但贴近生产形态、用于参考的 C++ Windows 服务示例,包含:配置管理、结构化日志、后台心跳,以及 Windows 服务控制管理器(SCM)集成。
+- 在 Windows 上完整演练开发闭环:Git 分支、Visual Studio C++ 工程、MSVC 编译器、批处理脚本,以及作为真实 Windows 服务的部署。
+
+### 1.2 运行模式
+
+同一个可执行文件支持两种模式:
+
+| 模式 | 调用方式 | 使用场景 |
+|---|---|---|
+| 控制台模式 | `x64\Debug\DeviceConfigMonitorService.exe --console` | 开发、调试 |
+| Windows 服务模式 | 在管理员模式实现 `install.bat` 、`start.bat`、`query.bat`、`stop.bat`、`uninstall.bat` | 生产部署 |
+
+### 1.3 第一周范围
+
+| 天 | 主题 | 状态 |
+|---|---|---|
+| 第 1 天 | Git 基础、提交模板、VS C++ 工程初始化 | 已完成 |
+| 第 2 天 | AppConfig + JSON 配置加载/保存 + 错误处理 | 已完成 |
+| 第 3 天 | Logger 模块 + Heartbeat 工作线程 + 控制台模式 | 已完成 |
+| 第 4 天 | Windows 服务框架 + 控制脚本 | 已完成 |
+| 第 5 天 | 仓库清理、完整测试通过、文档整理 | 已完成 |
+
+## 2. Git 提交记录
+
+分支 `day5-cpp-service` 截至本报告前包含 39 次提交。以下是按用途分组的主要提交。完整日志可用 `git log --oneline --graph` 查看。
+
+### 2.1 第 1 天 - 工程初始化
+
+| 提交 | 提交信息 | 用途 |
+|---|---|---|
+| `1526fdc` | func: app: Add minimal entry program | 首个 C++ 入口,支持 `--console` |
+| `c453ee7` | docs: app: Add training project README | 初始 README |
+| `884a3cf` | docs: app: Supplement the training project README | README 更新 |
+
+### 2.2 第 2 天 - 配置模型与 JSON 解析器
+
+| 提交 | 提交信息 | 用途 |
+|---|---|---|
+| `8037d13` | func: app: Add application config model | AppConfig 结构体 + NLOHMANN_DEFINE_TYPE_INTRUSIVE |
+| `4048db1` | func: app: Add JSON config parser | 带错误处理的 LoadConfig / SaveConfig |
+| `3b604ff` | fix: app: Skip bare drive letter in EnsureConfigDirectory | 修复 `C:` pop_back 缺陷 |
+| `373c884` | fix: app: Move bare-drive check after pop_back in EnsureConfigDirectory | 完善同一修复 |
+| `28ad113` | fix: build: Add /utf-8 compiler flag for MSVC encoding | 修复 GBK 代码页 936 问题 |
+| `35efa45` | docs: app: Add English version of the training project README | README 本地化为英文 |
+
+### 2.3 第 3 天 - 日志与心跳
+
+| 提交 | 提交信息 | 用途 |
+|---|---|---|
+| `7ee0207` | func: app: Add service log writer | Logger 模块(Info/Warn/Error) |
+| `b4b6d89` | fix: app: Fix logger compile errors | 修复 `` 与私有成员访问 |
+| `00d80d8` | func: app: Add heartbeat worker | 带原子停止标志的 HeartbeatWorker |
+| `884a3cf` | docs: app: Supplement the training project README | 更新README |
+
+### 2.4 第 4 天 - Windows 服务框架
+
+| 提交 | 提交信息 | 用途 |
+|---|---|---|
+| `3ca2703` | func: app: Add Windows service framework | ServiceMain / ServiceCtrlHandler / RunServiceBody |
+| `12f47d4` | conf: app: Add service control scripts | install / start / stop / query / uninstall .bat |
+| `555b50f` | fix: app: Add missing `` and `` to service_main.h | 编译修复 |
+| `b237f8b` | fix: app: Make service_main.h self-contained on Windows | 同一修复,完善 |
+| `35cfcf4` | fix: app: Use SERVICE_TABLE_ENTRYW explicitly for wide-char service name | A/W 签名不匹配修复 |
+| `8c1984c` | fix: app: Split STOP and SHUTDOWN cases to avoid es.78 fallthrough warning | C++ 核心准则合规 |
+| `cde4f84` | fix: app: Add const_cast for L"..." literal in SERVICE_TABLE_ENTRYW | const 正确性修复 |
+| `484edb0` | fix: conf: Remove Chinese comments from .bat scripts to fix encoding | GBK cmd.exe 编码问题 |
+| `e770fe8` | fix: conf: Fix binary path in install.bat (one more parent level) | 路径深度修复 |
+| `5e3fea6` | fix: conf: Use absolute path to sc.exe in all scripts | `sc` 不在 PATH 问题修复 |
+| `d157512` | style: app: Modify Chinese comments to English | 代码风格与 README 对齐 |
+| `12e73f9` | docs: app: Update the contents of the README | 更新 README |
+
+### 2.5 第 5 天 - 清理、文档与汇报
+
+| 提交 | 提交信息 | 用途 |
+|---|---|---|
+| `d2c6d48` | Merge branch 'week1-cpp-service' of origin | 误操作合并分支 |
+| `f92443a` | docs: app: Update README with full project documentation and FAQ | 更新README为最终版 |
+| `72a25cb` | `docs: app: Add service test report` | 新增:`docs/test_report.md` |
+| `794aad9` | `docs: app: Add week 1 summary` | 新增:`docs/week1_summary.md` |
+| `f7eba97` | docs: app: Add Chinese version of report and summary report | 新增中文版报告文件 |
+| `6ea3d3a` | docs: app: Update the text content | 更新文件 |
+| `4faa79b` | docs: app: Update the text content | 更新测试报告和汇报文件 |
+
+### 2.6 提交规范
+
+- 所有提交信息遵循 `TYPE: app: NOTICE:SUBJECT` 模板。
+- 使用过的 `TYPE` 值:`func`、`fix`、`docs`、`build`、`style`、`conf`、`merge`。
+- 主题使用英文,简短,且描述单一改动。
+- 没有出现 “update” / “fix bug” / “test” / “WIP” 这类信息。
+
+## 3. 工程结构
+
+仓库采用扁平布局(`src/` 存放 C++ 源文件,`scripts/`、`docs/`、`third_party/` 为子目录)。
+
+```
+DeviceConfigMonitorService/
++- DeviceConfigMonitorService.sln
++- README.md
++- .gitignore
++- .gitattributes
++- x64/ # 构建输出(已 gitignore)
++- DeviceConfigMonitorService/
+ +- src/ # 源码(C++ 实现文件)
+ | +- main.cpp # 入口:--console 或 SCM 分发
+ | +- config.cpp # AppConfig + JSON 加载/保存
+ | +- logger.cpp # Logger
+ | +- heartbeat_worker.cpp # HeartbeatWorker
+ | +- service_main.cpp # ServiceMain + ServiceCtrlHandler
+ +- config.h # AppConfig 头文件(与 src/config.cpp 配对)
+ +- logger.h # 与 src/logger.cpp 配对
+ +- heartbeat_worker.h # 与 src/heartbeat_worker.cpp 配对
+ +- service_main.h # 与 src/service_main.cpp 配对
+ +- config.example.json # 配置样例
+ +- DeviceConfigMonitorService.vcxproj
+ +- DeviceConfigMonitorService.vcxproj.filters
+ +- scripts/ # 服务控制 .bat 文件
+ | +- install.bat
+ | +- uninstall.bat
+ | +- start.bat
+ | +- stop.bat
+ | +- query.bat
+ +- docs/ # 文档
+ | +- test_report_zh.md
+ | +- week1_summary_zh.md
+ +- third_party/ # 第三方依赖库
+ +- nlohmann/
+ +- json.hpp # nlohmann/json v3.12.0(仅头文件)
+```
+
+### 3.1 模块职责
+
+| 模块 | 文件 | 用途 |
+|---|---|---|
+| 配置 | `config.h`(根)/`src/config.cpp` | `AppConfig` 结构体、`LoadConfig`、`SaveConfig`、`PrintConfigSummary` |
+| 日志 | `logger.h`(根)/`src/logger.cpp` | 按日轮转的日志写入器,`Info` / `Warn` / `Error` |
+| 心跳 | `heartbeat_worker.h`(根)/`src/heartbeat_worker.cpp` | 写入周期性心跳记录的后台 `std::thread` |
+| 服务框架 | `service_main.h`(根)/`src/service_main.cpp` | `ServiceMain`、`ServiceCtrlHandler`、状态上报 |
+| 主程序 | `src/main.cpp` | 参数解析、模式分发 |
+| 脚本 | `scripts/*.bat` | 服务生命周期脚本(需管理员权限) |
+| 文档 | `docs/*.md` | 测试报告与周报 |
+| 第三方库 | `third_party/nlohmann/json.hpp` | JSON 解析(单头文件) |
+
+### 3.2 构建配置
+
+- `vcxproj` 有 4 个配置:`Debug|x64`、`Debug|x86`、`Release|x64`、`Release|x86`。
+- 全部 4 个配置均使用 `/std:c++17 /utf-8 /EHsc`。
+- `/utf-8` 选项在中文 Windows(代码页 936)下处理非 ASCII 字符至关重要。详见第 8.1 节。
+
+## 4. 配置与 JSON
+
+### 4.1 文件位置
+
+配置文件从该路径读取并写入:
+
+```
+C:\ProgramData\NanningTraining\DeviceConfigMonitorService\config.json
+```
+
+使用 `C:\ProgramData` 是因为它对交互式用户和 LocalSystem 账户(Windows 服务所使用的账户)都可写。
+
+### 4.2 生成、读取、写入流程
+
+1. **生成:** 启动时,`LoadConfig()` 通过 `std::ifstream` 打开 `config.json`。若文件不存在,则打印一条警告,生成默认的 `AppConfig`,并调用 `SaveConfig()` 将默认值写入磁盘。
+2. **读取:** 文件内容读入 `std::string`,由 `nlohmann::json::parse()` 解析,再通过 `j.get()` 反序列化为 `AppConfig` 实例。
+3. **写入:** `SaveConfig()` 先调用 `EnsureConfigDirectory()` 递归创建父目录(跳过裸盘符),再将 `AppConfig` 序列化为 2 空格缩进的 JSON 字符串,通过 `std::ofstream` 以 `std::ios::trunc` 写入。
+
+### 4.3 异常处理
+
+`LoadConfig()` 捕获四类异常:
+
+| 异常 | 触发条件 | 行为 |
+|---|---|---|
+| `json::parse_error` | JSON 格式错误 | 回退到默认值 |
+| `json::out_of_range` | 缺少必需字段 | 回退到默认值 |
+| `json::type_error` | 字段类型错误 | 回退到默认值 |
+| `std::exception` | 任何其他错误 | 回退到默认值 |
+
+在以上四种情况下,程序都会向 stderr 记录一条带可读说明的 `[ERROR]`,打印 `[INFO] Falling back to default configuration.`,然后继续运行。程序绝不会因错误配置而崩溃。
+
+## 5. 日志与心跳
+
+### 5.1 日志路径与格式
+
+- **路径:** `AppConfig.LogPath`(默认:`C:\ProgramData\NanningTraining\DeviceConfigMonitorService\logs`)
+- **文件命名:** `service-YYYY-MM-DD.log`(每天一个文件)
+- **行格式:** `[YYYY-MM-DD HH:MM:SS.mmm] [LEVEL] message`
+- **级别:** `INFO`、`WARN`、`ERROR`
+- **自动轮转:** 每次写入时检测跨天;当天变化时关闭并重新打开文件。
+- **目录自动创建:** `EnsureDirectory()` 在首次写入时创建缺失的中间目录,跳过裸盘符。
+
+### 5.2 线程安全
+
+Logger 使用 `std::mutex` 串行化所有文件和控制台写入。多个线程(例如心跳工作线程和主循环)可以并发调用 `Logger::Info()` 而不会损坏数据。
+
+### 5.3 启动日志
+
+服务启动时,会写入以下记录:
+
+```
+[INFO] Service starting up...
+[INFO] Configuration loaded. DeviceId=..., ServiceName=..., LogPath=...
+[Logger] Log file: C:\ProgramData\...\logs\service-YYYY-MM-DD.log
+[INFO] Logger initialized. LogDir=...
+[INFO] Heartbeat worker starting. DeviceId=..., interval=Ns
+[INFO] Service entered main loop.
+```
+
+### 5.4 心跳日志
+
+每次心跳写入:
+
+```
+[INFO] [Heartbeat] DeviceId=..., ServiceName=...
+```
+
+- 首次心跳立即发出,之后工作线程等待下一个间隔。
+- 等待被切分为 200 毫秒的小片,以保持较低的停止延迟(`Stop` 到工作线程退出之间最多 200 毫秒)。
+- 若 `EnableHeartbeat=false`,仅记录一行信息:`Heartbeat is disabled (EnableHeartbeat=false).`
+- 若 `HeartbeatIntervalSeconds <= 0`,则自动设置间隔为 5 秒。
+
+### 5.5 停止日志
+
+在服务关闭时(控制台模式的 Ctrl+C,或服务模式下的 `SERVICE_CONTROL_STOP` / `SHUTDOWN`):
+
+```
+[INFO] Stop signal received. Shutting down...
+[INFO] Heartbeat worker stopped.
+[INFO] Service stopped.
+```
+
+在 “Service stopped.” 之后,程序调用 `Logger::Shutdown()` 刷新并关闭文件,不再有任何后续写入。
+
+## 6. Windows 服务
+
+### 6.1 服务身份
+
+| 字段 | 值 |
+|---|---|
+| 服务名 | `DeviceConfigMonitorService` |
+| 显示名 | `Device Config Monitor Service` |
+| 启动类型 | `auto`(随 Windows 自动启动) |
+| 服务账户 | `LocalSystem`(`sc create` 的默认账户) |
+
+### 6.2 服务主函数与控制回调
+
+`ServiceMain(DWORD, LPWSTR*)` 是由 SCM 调用的入口点:
+
+1. 注册 `ServiceCtrlHandler` 接收控制码。
+2. 初始化 `SERVICE_STATUS` 字段。
+3. 上报 `SERVICE_START_PENDING`(此时 `dwControlsAccepted = 0`)。
+4. 启动一个运行业务主体的 `std::thread`。
+5. 从工作线程中上报 `SERVICE_RUNNING`(此时 `dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN`)。
+6. 调用 `RunServiceBody()`,阻塞直到停止标志被置位。
+7. 业务主体返回后,上报 `SERVICE_STOPPED`。
+8. ServiceMain 返回,进程退出。
+
+`ServiceCtrlHandler(DWORD)` 处理控制码:
+
+- `SERVICE_CONTROL_STOP`:上报 `STOP_PENDING`,置位停止标志。
+- `SERVICE_CONTROL_SHUTDOWN`:与 STOP 相同。
+- 其他控制码:忽略(default 分支)。
+
+### 6.3 安装 / 启动 / 查询 / 停止 / 卸载
+
+所有脚本位于 `DeviceConfigMonitorService/scripts/`,使用 `%~dp0` 表示相对路径(因此工程目录可以自由移动)。
+
+```bat
+:: 注册
+scripts\install.bat
+
+:: 启动(开机自动启动,此处为手动启动)
+scripts\start.bat
+
+:: 查询(sc query)-> STATE、PID、dwControlsAccepted
+scripts\query.bat
+
+:: 停止(发送 SERVICE_CONTROL_STOP)
+scripts\stop.bat
+
+:: 卸载(若正在运行则先停止,再删除服务)
+scripts\uninstall.bat
+```
+
+所有脚本都需要**管理员**命令提示符,因为 `sc.exe` 需要 `OpenSCManager` 权限。`install.bat` 包含一个管理员检查:尝试向 `%SystemRoot%\System32` 写入临时文件并回滚;非管理员运行时失败并提示 `[ERROR] This script must be run as Administrator.`
+
+### 6.4 为什么 RunServiceBody 是共用的
+
+业务主体(`LoadConfig -> Logger::Init -> HeartbeatWorker.Start -> 主循环 -> 最后关闭`)在控制台模式和服务模式下完全相同。唯一的区别是停止标志的来源:
+
+| 模式 | 停止标志来源 |
+|---|---|
+| 控制台 | `SIGINT` / `SIGTERM` -> `ConsoleSignalHandler` |
+| 服务 | `SERVICE_CONTROL_STOP` -> `ServiceCtrlHandler` |
+
+两者都调用同一个 `RunServiceBody(std::atomic& stopFlag)`。
+
+## 7. 测试结果
+
+完整细节与逐步输出见 `docs/test_report_zh.md`,[点击查看测试详情](./test_report_zh.md#31-仓库卫生)。摘要如下:
+
+| 类别 | 总数 | 通过 | 失败 |
+|---|---|---|---|
+| 仓库卫生 | 6 | 6 | 0 |
+| 构建验证 | 4 | 4 | 0 |
+| 控制台模式(B-1 至 B-5) | 5 | 5 | 0 |
+| 脚本路径处理(E-1) | 1 | 1 | 0 |
+| Windows 服务(第 4 天结果) | 8 | 8 | 0 |
+| **合计** | **24** | **24** | **0** |
+
+### 7.1 未解决的问题
+
+**问题1:** 在后面提交改动时,不知道为什么自动进行了一次合并。然而Visual Studio和git bash都中并未找到提交记录。
+
+| `d2c6d48` | Merge branch 'week1-cpp-service' of origin | 误操作合并分支 |
+
+## 8. 问题复盘
+
+本周遇到并解决了一些使用 git 和创建服务程序时的问题。每个问题都在此记录原始现象、定位过程和最终修复。
+
+### 8.1 初次使用 git 时的问题(第 1 天)
+
+**现象1:** 使用 git bash 时,未建立vs工程文件,没有明确的项目意识。
+
+第一次接触 git 还没有完全理解 git 的操作方法,直接跟着“Git安装与使用新手教程”跑,完全没有联想到Visual Studio,在执行 `git clone`时才发现陷入了思维惯性。
+
+**解决:** 找到第一周项目计划书仔细阅读后,理解到 git bash 提供的作用。后需根据项目计划书和ai的帮助,一步步开始在Visual Studio上创建项目工程。
+
+**现象2:** 远程仓库拉取问题。
+
+由于初次使用 git ,没有理解清楚仓库相关概念,后续在github上随机克隆到的本地仓库发现没有上传分支的权限,导致 commit 失败。
+
+**解决:** 在b站上学习了仓库和 git 的相关教程后,理解了git 、github 、Visual Studio、vs code 的关联协作工作能力,学会了相关工具的使用方法。
+
+**现象3:** 远程仓库推送问题。
+
+使用 vs code 推送代码到 github 上的仓库时,推送失败,没有git ssl证书。
+
+**解决:** 把克隆 HTTPS 的方式改为使用 SSH 的方式,在 github 注册了一个 SSH 密钥,并将本地与远程链接改为 SSH 链接。
+
+### 8.2 MSVC 代码页 936 / UTF-8 问题(第 2 天)
+
+**现象:** `cl.exe` 报告了多个类似错误:
+
+```
+warning C4828: The file contains a character that is illegal in the
+current source character set (codepage 936).
+```
+
+**定位:** 中文 Windows 默认使用代码页 936(GBK)。MSVC 按系统代码页解释源文件。注释中的非 ASCII 字符触发了 C4828,在 `/W4` 下升级为错误。
+
+**修复:** 在全部 4 个构建配置(`Debug|x64`、`Debug|x86`、`Release|x64`、`Release|x86`)的编译器选项中增加 `/utf-8`。这告诉 MSVC 无论系统代码页如何,都按 UTF-8 解释源文件。
+
+**提交:** `28ad113` “fix: build: Add /utf-8 compiler flag for MSVC encoding”
+
+### 8.3 服务模式编译错误(第 4 天)
+
+**现象:** 添加 `service_main.h` 后,工程产生 100+ 编译错误,包括:
+
+- `error C2027: use of undefined type 'void'`
+- `error C2065: 'ServiceMain': undeclared identifier`
+- `error C2371: 'DWORD': redefinition`
+- `warning C26818 (es.78): fallthrough in switch`
+- `error C2440: cannot convert from 'const wchar_t [27]' to 'LPWSTR'`
+
+**定位:** 逐一识别并修复了五个不同的问题:
+
+1. `service_main.h` 未包含 ``,因此 `DWORD`、`LPWSTR`、`SERVICE_STATUS` 等类型不完整。修复:用 `#ifdef _WIN32` 保护后加入该头文件。
+2. `SERVICE_TABLE_ENTRY` 是 A/W 分发宏;本工程的服务主函数使用宽字符签名,因此需要显式的 `SERVICE_TABLE_ENTRYW`。
+3. `ServiceCtrlHandler` 相邻 case 标签缺少 `break`,触发 C26818(es.78)。修复:将 STOP 和 SHUTDOWN 拆成独立 case,各自以 `break` 结束。
+4. `lpServiceName` 是 `LPWSTR`(非 const),但 `L"..."` 是 `const wchar_t[]`。修复:`const_cast(L"...")`。
+5. (已记录)`.bat` 脚本包含中文注释,被 GBK cmd.exe 误读。
+
+**修复:** 五个独立的 `fix: app:` 提交,每个对应一个根因。最终构建在 `/W4 /utf-8 /std:c++17` 下干净。
+
+**提交:** `555b50f`、`b237f8b`、`35cfcf4`、`8c1984c`、`cde4f84`
+
+### 8.4 install.bat 编码与路径问题(第 4 天)
+
+**现象 1:** `install.bat` 失败,报错:
+
+```
+'ice' 不是内部或外部命令,也不是可运行的程序
+[ERROR] sc create failed
+```
+
+**定位:** 脚本包含 UTF-8 编码的中文注释。cmd.exe(代码页 936)将多字节序列解释为独立的 ASCII 字符,产生无意义的命令名。
+
+**修复:** 将全部 5 个脚本改写为纯 ASCII。所有面向用户的说明移至 README 和 docs/ 目录。
+
+**现象 2:** 即使 exe 已构建,`install.bat` 仍报告 `Binary not found`。
+
+**定位:** 脚本位于 `scripts/`,而 exe 在解决方案根的 `x64/Debug/`。原路径用的是 `..\x64\...`,只向上一级。正确路径是 `..\..\x64\...`。
+
+**修复:** 将 `BIN_PATH` 改为 `%~dp0..\..\x64\Debug\...`。
+
+**现象 3:** `install.bat` 报告 `OpenSCManager failed 5: 拒绝访问`(拒绝访问)。
+
+**定位:** 脚本未以管理员身份运行。
+
+**修复:** 在 `install.bat` 开头增加管理员检查:尝试向 `%SystemRoot%\System32` 写入临时文件。若失败,脚本以明确的 `[ERROR] This script must be run as Administrator.` 提示后中止。同时硬编码 `SC=C:\Windows\System32\sc.exe`,使脚本在 `sc` 不在当前 PATH 时也能工作。并使用管理员打开CMD进行测试。
+
+**提交:** `484edb0`、`e770fe8`、`5e3fea6`
+
+### 8.5 项目文件损坏(第 4 天)
+
+**现象 1:** 解决方案出错,区域显示文件已卸载。
+
+
+**定位:** 检查文件更改前的其他操作,检查项目文件完整性,发现是`.vcxproj`文件丢失了。
+
+**修复:** 将`.vcxproj`文件复原,再重新加载解决方案。
+
+## 9. 自我评估
+
+### 9.1 最熟悉的部分
+
+- 基础语法、命令行模式、github 使用方式
+- 使用ai工具帮助项目计划实现
+- 能够使用 git 进行基本的拉取、提交、推送功能
+- 熟悉了 Visual Studio 在工作流程中的使用方法
+
+### 9.2 后续还需练习的部分
+
+- **基本工作流程:** 多加练习 git 、 Visual Studio 等相关工具的工作交互流程,深入掌握调试工具,理解项目配置文件结构。
+- **Windows 服务理解:** 深化对Windows 服务控制管理器的状态机机制的理解。
+- **基本项目意识:** 学会理解项目相关工具的基本使用方式和场景,学会看懂编写程序时的报错信息,学会记录任务中的关键节点以便于后续排查错误。
+
+## 10. 附件清单
+
+| 文件名 | 类型 | 描述 |
+|---|---|---|
+| `README.md` | Markdown | 工程 README,构建与运行说明、常见问题 |
+| `docs/test_report_zh.md` | Markdown | 完整测试报告中文版 |
+| `docs/week1_summary_zh.md` | Markdown | 汇报文件中文版 |
+| `docs/screenshots/` | 目录 | 测试运行时拍摄的截图 |
+| `x64/Debug/DeviceConfigMonitorService.exe` | 二进制 | 编译产物(1.18 MB) |
+
+### 截图检查清单
+
+- [ ] 
+- [ ] (`git log --oneline --graph`)
+- [ ] (心跳输出)
+- [ ] (自动生成)
+- [ ] (错误下回退)
+- [ ] (`sc query` STATE 4)
+- [ ] (启动 + 心跳 + 停止)
+
+报告结束。
diff --git a/DeviceConfigMonitorService/heartbeat_worker.h b/DeviceConfigMonitorService/heartbeat_worker.h
new file mode 100644
index 0000000..84084be
--- /dev/null
+++ b/DeviceConfigMonitorService/heartbeat_worker.h
@@ -0,0 +1,49 @@
+//============================================================================
+// DeviceConfigMonitorService - Heartbeat worker
+//============================================================================
+// Day 3 task: periodic heartbeat log writer.
+// - Reads AppConfig.EnableHeartbeat and HeartbeatIntervalSeconds
+// - When EnableHeartbeat is true, calls Logger::Info() every N seconds
+// - When HeartbeatIntervalSeconds <= 0, uses default 5 seconds
+// - Runs in a background std::thread; supports clean stop via atomic flag
+//============================================================================
+
+#pragma once
+
+#include
+#include
+#include
+
+#include "config.h"
+
+//============================================================================
+// HeartbeatWorker - background heartbeat thread
+//============================================================================
+class HeartbeatWorker {
+public:
+ HeartbeatWorker();
+ ~HeartbeatWorker();
+
+ // Start: takes a copy of the config (avoids external lifetime issues)
+ // Internally decides whether to actually start the thread based on EnableHeartbeat
+ void Start(const AppConfig& config);
+
+ // Stop: set stop flag + join thread
+ // Safe to call multiple times
+ void Stop();
+
+ // Whether the worker is currently running
+ bool IsRunning() const { return running_.load(); }
+
+private:
+ // Thread main loop
+ void Loop();
+
+ // Helper: normalize interval to a valid value (<=0 → 5)
+ static int NormalizeInterval(int seconds);
+
+ std::thread thread_;
+ std::atomic running_;
+ std::atomic stopRequested_;
+ AppConfig config_;
+};
diff --git a/DeviceConfigMonitorService/logger.h b/DeviceConfigMonitorService/logger.h
new file mode 100644
index 0000000..100124b
--- /dev/null
+++ b/DeviceConfigMonitorService/logger.h
@@ -0,0 +1,66 @@
+//============================================================================
+// DeviceConfigMonitorService - Logger module
+//============================================================================
+// Day 3 task: log writer with Info/Warn/Error levels.
+// - Log file path is taken from AppConfig.LogPath
+// - Log directory is created automatically if it doesn't exist
+// - Each line: [YYYY-MM-DD HH:MM:SS.mmm] [LEVEL] message
+// - Thread-safe: a mutex guards the underlying ofstream
+//============================================================================
+
+#pragma once
+
+#include
+#include
+#include
+
+//============================================================================
+// LogLevel - severity levels
+//============================================================================
+enum class LogLevel {
+ Info,
+ Warn,
+ Error,
+};
+
+//============================================================================
+// Logger - singleton-style log writer
+//============================================================================
+// Design:
+// - Init(logPath) specifies the log directory; log file name is fixed as service-.log
+// e.g. service-2026-07-08.log, one file per day to prevent unbounded growth
+// - Info/Warn/Error are three public methods consistent with std::cout (stream-like API)
+// - Dual output: file write + console output (convenient during development)
+// - Thread-safe: all write operations serialized by a mutex
+//============================================================================
+class Logger {
+public:
+ // Initialize log directory (path from AppConfig.LogPath)
+ // Returns true on success; false on directory creation failure (writes are silently skipped)
+ static bool Init(const std::string& logDir);
+
+ // Explicit shutdown (call on program exit to ensure buffer flush)
+ static void Shutdown();
+
+ // Three severity levels for log writing
+ static void Info(const std::string& message);
+ static void Warn(const std::string& message);
+ static void Error(const std::string& message);
+
+ // Internal: level string + message write
+ static void Write(LogLevel level, const std::string& message);
+
+ // Status queries (for self-check use)
+ static bool IsInitialized();
+ static const std::string& GetCurrentLogFile();
+
+private:
+ // Cross-day detection: switch to a new file when day changes
+ static void RotateIfNeededInternal();
+
+ static std::string s_logDir;
+ static std::string s_currentFile;
+ static std::ofstream s_ofs;
+ static std::mutex s_mutex;
+ static bool s_initialized;
+};
diff --git a/DeviceConfigMonitorService/main.cpp b/DeviceConfigMonitorService/main.cpp
deleted file mode 100644
index e69de29..0000000
diff --git a/DeviceConfigMonitorService/scripts/install.bat b/DeviceConfigMonitorService/scripts/install.bat
new file mode 100644
index 0000000..7cd2365
--- /dev/null
+++ b/DeviceConfigMonitorService/scripts/install.bat
@@ -0,0 +1,51 @@
+@echo off
+setlocal
+
+set "SERVICE_NAME=DeviceConfigMonitorService"
+set "DISPLAY_NAME=Device Config Monitor Service"
+set "BIN_PATH=%~dp0..\..\x64\Debug\DeviceConfigMonitorService.exe"
+
+REM Use absolute path to sc.exe so it works even when System32
+REM is somehow not in PATH.
+set "SC=C:\Windows\System32\sc.exe"
+
+echo [install] Registering service...
+echo Service name : %SERVICE_NAME%
+echo Display name : %DISPLAY_NAME%
+echo Binary path : %BIN_PATH%
+
+if not exist "%BIN_PATH%" (
+ echo [ERROR] Binary not found: %BIN_PATH%
+ echo Please build the project in Visual Studio first.
+ exit /b 1
+)
+
+if not exist "%SC%" (
+ echo [ERROR] sc.exe not found at %SC%
+ exit /b 1
+)
+
+REM Check for admin: try to write to a system directory
+echo. > "%SystemRoot%\System32\dcms_install_test.tmp" 2>nul
+if errorlevel 1 (
+ echo [ERROR] This script must be run as Administrator.
+ del "%SystemRoot%\System32\dcms_install_test.tmp" 2>nul
+ exit /b 1
+)
+del "%SystemRoot%\System32\dcms_install_test.tmp" 2>nul
+
+"%SC%" query %SERVICE_NAME% >nul 2>&1
+if not errorlevel 1 (
+ echo [WARN] Service already exists, deleting first...
+ "%SC%" delete %SERVICE_NAME%
+ timeout /t 2 /nobreak >nul
+)
+
+"%SC%" create %SERVICE_NAME% binPath= "%BIN_PATH%" DisplayName= "%DISPLAY_NAME%" start= auto
+if errorlevel 1 (
+ echo [ERROR] sc create failed.
+ exit /b 1
+)
+
+echo [install] Service registered. Use start.bat to start.
+endlocal
diff --git a/DeviceConfigMonitorService/scripts/query.bat b/DeviceConfigMonitorService/scripts/query.bat
new file mode 100644
index 0000000..04e9c1d
--- /dev/null
+++ b/DeviceConfigMonitorService/scripts/query.bat
@@ -0,0 +1,16 @@
+@echo off
+setlocal
+
+set "SERVICE_NAME=DeviceConfigMonitorService"
+set "SC=C:\Windows\System32\sc.exe"
+
+echo [query] Querying service: %SERVICE_NAME%
+echo.
+
+"%SC%" query %SERVICE_NAME%
+if errorlevel 1 (
+ echo [WARN] Service not installed.
+ exit /b 0
+)
+
+endlocal
diff --git a/DeviceConfigMonitorService/scripts/start.bat b/DeviceConfigMonitorService/scripts/start.bat
new file mode 100644
index 0000000..1c75da5
--- /dev/null
+++ b/DeviceConfigMonitorService/scripts/start.bat
@@ -0,0 +1,22 @@
+@echo off
+setlocal
+
+set "SERVICE_NAME=DeviceConfigMonitorService"
+set "SC=C:\Windows\System32\sc.exe"
+
+echo [start] Starting service: %SERVICE_NAME%
+
+"%SC%" query %SERVICE_NAME% >nul 2>&1
+if errorlevel 1 (
+ echo [ERROR] Service not installed. Run install.bat first.
+ exit /b 1
+)
+
+"%SC%" start %SERVICE_NAME%
+if errorlevel 1 (
+ echo [ERROR] sc start failed. Run as Administrator?
+ exit /b 1
+)
+
+echo [start] Service started. Use query.bat to check status.
+endlocal
diff --git a/DeviceConfigMonitorService/scripts/stop.bat b/DeviceConfigMonitorService/scripts/stop.bat
new file mode 100644
index 0000000..0456aac
--- /dev/null
+++ b/DeviceConfigMonitorService/scripts/stop.bat
@@ -0,0 +1,22 @@
+@echo off
+setlocal
+
+set "SERVICE_NAME=DeviceConfigMonitorService"
+set "SC=C:\Windows\System32\sc.exe"
+
+echo [stop] Stopping service: %SERVICE_NAME%
+
+"%SC%" query %SERVICE_NAME% >nul 2>&1
+if errorlevel 1 (
+ echo [WARN] Service not installed.
+ exit /b 0
+)
+
+"%SC%" stop %SERVICE_NAME%
+if errorlevel 1 (
+ echo [ERROR] sc stop failed. Run as Administrator?
+ exit /b 1
+)
+
+echo [stop] Stop signal sent. Use query.bat to confirm.
+endlocal
diff --git a/DeviceConfigMonitorService/scripts/uninstall.bat b/DeviceConfigMonitorService/scripts/uninstall.bat
new file mode 100644
index 0000000..b15c59a
--- /dev/null
+++ b/DeviceConfigMonitorService/scripts/uninstall.bat
@@ -0,0 +1,25 @@
+@echo off
+setlocal
+
+set "SERVICE_NAME=DeviceConfigMonitorService"
+set "SC=C:\Windows\System32\sc.exe"
+
+echo [uninstall] Removing service: %SERVICE_NAME%
+
+"%SC%" query %SERVICE_NAME% >nul 2>&1
+if errorlevel 1 (
+ echo [WARN] Service not found.
+ exit /b 0
+)
+
+"%SC%" stop %SERVICE_NAME% >nul 2>&1
+timeout /t 3 /nobreak >nul
+
+"%SC%" delete %SERVICE_NAME%
+if errorlevel 1 (
+ echo [ERROR] sc delete failed. Run as Administrator?
+ exit /b 1
+)
+
+echo [uninstall] Service removed.
+endlocal
diff --git a/DeviceConfigMonitorService/service_main.h b/DeviceConfigMonitorService/service_main.h
new file mode 100644
index 0000000..1d30a54
--- /dev/null
+++ b/DeviceConfigMonitorService/service_main.h
@@ -0,0 +1,38 @@
+//============================================================================
+// DeviceConfigMonitorService - Windows Service framework
+//============================================================================
+// Day 4 task: extend --console mode into a real Windows Service.
+// - ServiceMain is the entry point Windows SCM calls after StartService()
+// - ServiceCtrlHandler receives control codes (STOP / SHUTDOWN / ...)
+// - RunServiceBody() holds the shared business logic (config + log + heartbeat)
+// used by both --console and service modes
+//============================================================================
+
+#pragma once
+
+#include
+
+// Windows types needed for ServiceMain / ServiceCtrlHandler signatures.
+// Always include on Windows; the project is Windows-only anyway.
+#ifdef _WIN32
+#include // WINAPI, DWORD, LPWSTR, SERVICE_STATUS, ...
+#endif
+
+//============================================================================
+// Public API
+//============================================================================
+
+// Shared business body (used by both console and service modes)
+// Flow: LoadConfig -> Logger::Init -> HeartbeatWorker.Start -> main loop -> graceful shutdown
+// stopFlag is controlled by the caller: Ctrl+C atomic in console mode, CtrlHandler in service mode
+// Inner loop checks stopFlag every 200ms; worst-case exit latency is 200ms
+void RunServiceBody(std::atomic& stopFlag);
+
+// Windows Service entry point (called by SCM, **not directly from main**)
+// Registers ServiceCtrlHandler + starts the main loop
+// Note: LPSERVICE_MAIN_FUNCTION is a function-pointer type defined in windows.h
+// The function signature must exactly match LPSERVICE_MAIN_FUNCTION
+void WINAPI ServiceMain(DWORD argc, LPWSTR* argv);
+
+// Control callback: handles SERVICE_CONTROL_STOP / SERVICE_CONTROL_SHUTDOWN
+void WINAPI ServiceCtrlHandler(DWORD ctrlCode);
diff --git a/DeviceConfigMonitorService/src/config.cpp b/DeviceConfigMonitorService/src/config.cpp
new file mode 100644
index 0000000..8e23e64
--- /dev/null
+++ b/DeviceConfigMonitorService/src/config.cpp
@@ -0,0 +1,176 @@
+#include "config.h"
+
+#include
+#include
+#include
+
+// Windows directory creation
+#ifdef _WIN32
+#include
+#else
+#include
+#endif
+
+namespace {
+
+// Recursively create the directory containing the config file
+bool EnsureConfigDirectory(const std::string& filePath) {
+ size_t lastSlash = filePath.find_last_of("\\/");
+ if (lastSlash == std::string::npos) {
+ return true; // no directory portion
+ }
+ std::string dirPath = filePath.substr(0, lastSlash);
+
+#ifdef _WIN32
+ // Windows: recursively create directories
+ std::string pathCopy = dirPath;
+ for (auto& ch : pathCopy) {
+ if (ch == '/') ch = '\\';
+ }
+
+ std::string current;
+ for (size_t i = 0; i < pathCopy.size(); ++i) {
+ current += pathCopy[i];
+ if (pathCopy[i] == '\\' || i == pathCopy.size() - 1) {
+ // Strip trailing backslash to get the directory name to create
+ if (current.back() == '\\') {
+ current.pop_back();
+ }
+ // Skip bare drive letters like "C:" / "D:" which are invalid CreateDirectory targets
+ bool isBareDrive = (current.size() == 2 && current[1] == ':');
+ if (!isBareDrive && !current.empty()
+ && !CreateDirectoryA(current.c_str(), nullptr)) {
+ DWORD err = GetLastError();
+ if (err != ERROR_ALREADY_EXISTS) {
+ return false;
+ }
+ }
+ if (pathCopy[i] == '\\') {
+ current += '\\';
+ }
+ }
+ }
+ return true;
+#else
+ // Linux/macOS: mkdir -p
+ std::string cmd = "mkdir -p \"" + dirPath + "\"";
+ return system(cmd.c_str()) == 0;
+#endif
+}
+
+// Generate default configuration
+AppConfig GetDefaultConfig() {
+ AppConfig cfg;
+ cfg.DeviceId = "DEVICE-0001";
+ cfg.ServiceName = "DeviceConfigMonitorService";
+ cfg.EnableHeartbeat = true;
+ cfg.HeartbeatIntervalSeconds = 30;
+ cfg.LogLevel = "INFO";
+ cfg.LogPath = "C:\\ProgramData\\NanningTraining\\DeviceConfigMonitorService\\logs";
+ return cfg;
+}
+
+} // anonymous namespace
+
+AppConfig LoadConfig() {
+ AppConfig config;
+
+ std::ifstream ifs(CONFIG_FILE_PATH);
+ if (!ifs.is_open()) {
+ // config.json not found, generate default config
+ std::cerr << "[WARN] Config file not found at: " << CONFIG_FILE_PATH << std::endl;
+ std::cerr << "[INFO] Generating default configuration..." << std::endl;
+
+ config = GetDefaultConfig();
+
+ if (!SaveConfig(config)) {
+ std::cerr << "[ERROR] Failed to save default configuration." << std::endl;
+ } else {
+ std::cerr << "[INFO] Default configuration saved to: " << CONFIG_FILE_PATH << std::endl;
+ }
+ return config;
+ }
+
+ try {
+ std::string content((std::istreambuf_iterator(ifs)),
+ std::istreambuf_iterator());
+ ifs.close();
+
+ if (content.empty()) {
+ std::cerr << "[WARN] Config file is empty, using defaults." << std::endl;
+ config = GetDefaultConfig();
+ return config;
+ }
+
+ json j = json::parse(content);
+ config = j.get();
+
+ std::cerr << "[INFO] Configuration loaded from: " << CONFIG_FILE_PATH << std::endl;
+
+ } catch (const json::parse_error& e) {
+ std::cerr << "[ERROR] Failed to parse config file (JSON error): " << e.what() << std::endl;
+ std::cerr << "[INFO] Falling back to default configuration." << std::endl;
+ config = GetDefaultConfig();
+
+ } catch (const json::out_of_range& e) {
+ std::cerr << "[ERROR] Config field error: " << e.what() << std::endl;
+ std::cerr << "[INFO] Falling back to default configuration." << std::endl;
+ config = GetDefaultConfig();
+
+ } catch (const json::type_error& e) {
+ std::cerr << "[ERROR] Config type mismatch: " << e.what() << std::endl;
+ std::cerr << "[INFO] Falling back to default configuration." << std::endl;
+ config = GetDefaultConfig();
+
+ } catch (const std::exception& e) {
+ std::cerr << "[ERROR] Unexpected error loading config: " << e.what() << std::endl;
+ std::cerr << "[INFO] Falling back to default configuration." << std::endl;
+ config = GetDefaultConfig();
+ }
+
+ return config;
+}
+
+bool SaveConfig(const AppConfig& config) {
+ try {
+ if (!EnsureConfigDirectory(CONFIG_FILE_PATH)) {
+ std::cerr << "[ERROR] Failed to create config directory." << std::endl;
+ return false;
+ }
+
+ json j = config;
+ std::string jsonStr = j.dump(2);
+
+ std::ofstream ofs(CONFIG_FILE_PATH, std::ios::trunc);
+ if (!ofs.is_open()) {
+ std::cerr << "[ERROR] Failed to open config file for writing: "
+ << CONFIG_FILE_PATH << std::endl;
+ return false;
+ }
+
+ ofs << jsonStr;
+ ofs.close();
+
+ std::cerr << "[INFO] Configuration saved to: " << CONFIG_FILE_PATH << std::endl;
+ return true;
+
+ } catch (const std::exception& e) {
+ std::cerr << "[ERROR] Failed to save config: " << e.what() << std::endl;
+ return false;
+ }
+}
+
+void PrintConfigSummary(const AppConfig& config) {
+ std::cout << std::endl;
+ std::cout << "+------------------------------------------+" << std::endl;
+ std::cout << "| Configuration Summary |" << std::endl;
+ std::cout << "+------------------------------------------+" << std::endl;
+ std::cout << " DeviceId : " << config.DeviceId << std::endl;
+ std::cout << " ServiceName : " << config.ServiceName << std::endl;
+ std::cout << " EnableHeartbeat : " << (config.EnableHeartbeat ? "true" : "false") << std::endl;
+ std::cout << " HeartbeatIntervalSeconds: " << config.HeartbeatIntervalSeconds << "s" << std::endl;
+ std::cout << " LogLevel : " << config.LogLevel << std::endl;
+ std::cout << " LogPath : " << config.LogPath << std::endl;
+ std::cout << "+------------------------------------------+" << std::endl;
+ std::cout << std::endl;
+}
diff --git a/DeviceConfigMonitorService/src/heartbeat_worker.cpp b/DeviceConfigMonitorService/src/heartbeat_worker.cpp
new file mode 100644
index 0000000..62051c3
--- /dev/null
+++ b/DeviceConfigMonitorService/src/heartbeat_worker.cpp
@@ -0,0 +1,97 @@
+//============================================================================
+// DeviceConfigMonitorService - Heartbeat worker implementation
+//============================================================================
+
+#include "heartbeat_worker.h"
+#include "logger.h"
+
+#include
+
+//============================================================================
+// Constants
+//============================================================================
+namespace {
+constexpr int DEFAULT_HEARTBEAT_INTERVAL_SECONDS = 5;
+}
+
+//============================================================================
+// HeartbeatWorker
+//============================================================================
+HeartbeatWorker::HeartbeatWorker()
+ : running_(false)
+ , stopRequested_(false) {
+}
+
+HeartbeatWorker::~HeartbeatWorker() {
+ Stop();
+}
+
+int HeartbeatWorker::NormalizeInterval(int seconds) {
+ if (seconds <= 0) {
+ return DEFAULT_HEARTBEAT_INTERVAL_SECONDS;
+ }
+ return seconds;
+}
+
+void HeartbeatWorker::Start(const AppConfig& config) {
+ if (running_.load()) {
+ return; // already running
+ }
+
+ config_ = config;
+ stopRequested_ = false;
+
+ if (!config_.EnableHeartbeat) {
+ // Config disables heartbeat: do not start the thread
+ Logger::Info("Heartbeat is disabled (EnableHeartbeat=false).");
+ return;
+ }
+
+ int interval = NormalizeInterval(config_.HeartbeatIntervalSeconds);
+ Logger::Info("Heartbeat worker starting. DeviceId="
+ + config_.DeviceId
+ + ", interval=" + std::to_string(interval) + "s");
+
+ running_.store(true);
+ thread_ = std::thread(&HeartbeatWorker::Loop, this);
+}
+
+void HeartbeatWorker::Stop() {
+ if (!running_.load()) {
+ return;
+ }
+ stopRequested_.store(true);
+ if (thread_.joinable()) {
+ thread_.join();
+ }
+ running_.store(false);
+ Logger::Info("Heartbeat worker stopped.");
+}
+
+void HeartbeatWorker::Loop() {
+ int interval = NormalizeInterval(config_.HeartbeatIntervalSeconds);
+
+ // Emit one heartbeat immediately, then cycle on the interval
+ auto nextTime = std::chrono::steady_clock::now();
+
+ while (!stopRequested_.load()) {
+ // Write a heartbeat entry
+ Logger::Info("[Heartbeat] DeviceId=" + config_.DeviceId
+ + ", ServiceName=" + config_.ServiceName);
+
+ // Advance to the next time point
+ nextTime += std::chrono::seconds(interval);
+
+ // Short-interval sleep + stop flag check (avoids slow exit on long intervals)
+ // Sleep at most 200ms per iteration, checking frequently
+ while (!stopRequested_.load()) {
+ auto now = std::chrono::steady_clock::now();
+ if (now >= nextTime) break;
+
+ auto remaining = std::chrono::duration_cast(
+ nextTime - now).count();
+ auto sleepMs = (std::min)(static_cast(remaining), 200LL);
+ std::this_thread::sleep_for(std::chrono::milliseconds(sleepMs));
+ }
+ }
+}
diff --git a/DeviceConfigMonitorService/src/logger.cpp b/DeviceConfigMonitorService/src/logger.cpp
new file mode 100644
index 0000000..405059a
--- /dev/null
+++ b/DeviceConfigMonitorService/src/logger.cpp
@@ -0,0 +1,221 @@
+//============================================================================
+// DeviceConfigMonitorService - Logger implementation
+//============================================================================
+
+#include "logger.h"
+
+#include
+#include
+#include
+#include
+#include
+
+#ifdef _WIN32
+#include
+#endif
+
+//============================================================================
+// Static member definitions
+//============================================================================
+std::string Logger::s_logDir;
+std::string Logger::s_currentFile;
+std::ofstream Logger::s_ofs;
+std::mutex Logger::s_mutex;
+bool Logger::s_initialized = false;
+
+//============================================================================
+// Internal helper: create directory recursively
+// (Uses the same fix as config.cpp: skip bare drive letters)
+//============================================================================
+namespace {
+
+bool EnsureDirectory(const std::string& dirPath) {
+#ifdef _WIN32
+ std::string pathCopy = dirPath;
+ for (auto& ch : pathCopy) {
+ if (ch == '/') ch = '\\';
+ }
+
+ std::string current;
+ for (size_t i = 0; i < pathCopy.size(); ++i) {
+ current += pathCopy[i];
+ if (pathCopy[i] == '\\' || i == pathCopy.size() - 1) {
+ if (current.back() == '\\') {
+ current.pop_back();
+ }
+ // Skip bare drive letters "C:" / "D:"
+ bool isBareDrive = (current.size() == 2 && current[1] == ':');
+ if (!isBareDrive && !current.empty()
+ && !CreateDirectoryA(current.c_str(), nullptr)) {
+ DWORD err = GetLastError();
+ if (err != ERROR_ALREADY_EXISTS) {
+ return false;
+ }
+ }
+ if (pathCopy[i] == '\\') {
+ current += '\\';
+ }
+ }
+ }
+ return true;
+#else
+ std::string cmd = "mkdir -p \"" + dirPath + "\"";
+ return system(cmd.c_str()) == 0;
+#endif
+}
+
+// Level -> string
+const char* LevelToString(LogLevel level) {
+ switch (level) {
+ case LogLevel::Info: return "INFO";
+ case LogLevel::Warn: return "WARN";
+ case LogLevel::Error: return "ERROR";
+ default: return "?";
+ }
+}
+
+// Get current timestamp: [YYYY-MM-DD HH:MM:SS.mmm]
+std::string CurrentTimestamp() {
+ using namespace std::chrono;
+ auto now = system_clock::now();
+ auto t = system_clock::to_time_t(now);
+ auto millis = duration_cast(now.time_since_epoch()).count() % 1000;
+
+ std::tm tm_buf;
+#ifdef _WIN32
+ localtime_s(&tm_buf, &t);
+#else
+ localtime_r(&t, &tm_buf);
+#endif
+
+ std::ostringstream oss;
+ oss << std::put_time(&tm_buf, "%Y-%m-%d %H:%M:%S")
+ << '.' << std::setw(3) << std::setfill('0') << millis;
+ return oss.str();
+}
+
+// Generate today's log file name: service-YYYY-MM-DD.log
+std::string MakeLogFileName() {
+ using namespace std::chrono;
+ auto now = system_clock::now();
+ auto t = system_clock::to_time_t(now);
+ std::tm tm_buf;
+#ifdef _WIN32
+ localtime_s(&tm_buf, &t);
+#else
+ localtime_r(&t, &tm_buf);
+#endif
+
+ std::ostringstream oss;
+ oss << "service-" << std::put_time(&tm_buf, "%Y-%m-%d") << ".log";
+ return oss.str();
+}
+
+// Cross-day detection: if the file name differs from today's expected name, reopen the file
+// Note: This cannot access Logger private members from inside an anonymous namespace,
+// so it is implemented as a private static method (Logger::RotateIfNeededInternal below)
+
+} // namespace
+
+//============================================================================
+// Logger public API
+//============================================================================
+bool Logger::Init(const std::string& logDir) {
+ std::lock_guard lock(s_mutex);
+
+ s_logDir = logDir;
+
+ // 1) Ensure log directory exists
+ if (!EnsureDirectory(logDir)) {
+ std::cerr << "[Logger] Failed to create log directory: " << logDir << std::endl;
+ return false;
+ }
+
+ // 2) Open today's log file
+ s_currentFile = MakeLogFileName();
+ std::string fullPath = s_logDir + "\\" + s_currentFile;
+
+ s_ofs.open(fullPath, std::ios::app);
+ if (!s_ofs.is_open()) {
+ std::cerr << "[Logger] Failed to open log file: " << fullPath << std::endl;
+ return false;
+ }
+
+ s_initialized = true;
+ std::cerr << "[Logger] Log file: " << fullPath << std::endl;
+ return true;
+}
+
+// Internal: reopen file on day rollover (private static method of Logger)
+void Logger::RotateIfNeededInternal() {
+ std::string expected = MakeLogFileName();
+ if (expected != s_currentFile) {
+ if (s_ofs.is_open()) {
+ s_ofs.close();
+ }
+ s_currentFile = expected;
+ std::string fullPath = s_logDir + "\\" + expected;
+ s_ofs.open(fullPath, std::ios::app);
+ if (!s_ofs.is_open()) {
+ std::cerr << "[Logger] Failed to open log file: " << fullPath << std::endl;
+ }
+ }
+}
+
+void Logger::Shutdown() {
+ std::lock_guard lock(s_mutex);
+ if (s_ofs.is_open()) {
+ s_ofs.flush();
+ s_ofs.close();
+ }
+ s_initialized = false;
+}
+
+void Logger::Info(const std::string& message) {
+ Write(LogLevel::Info, message);
+}
+
+void Logger::Warn(const std::string& message) {
+ Write(LogLevel::Warn, message);
+}
+
+void Logger::Error(const std::string& message) {
+ Write(LogLevel::Error, message);
+}
+
+void Logger::Write(LogLevel level, const std::string& message) {
+ std::lock_guard lock(s_mutex);
+
+ if (!s_initialized) {
+ // Not initialized; output to stderr only, no file write
+ std::cerr << "[" << LevelToString(level) << "] " << message << std::endl;
+ return;
+ }
+
+ // Cross-day detection: switch to a new file when day changes
+ RotateIfNeededInternal();
+
+ std::string line = "[" + CurrentTimestamp() + "] ["
+ + LevelToString(level) + "] " + message;
+
+ // Write to file
+ if (s_ofs.is_open()) {
+ s_ofs << line << std::endl;
+ s_ofs.flush();
+ }
+
+ // Also output to console (convenient during development)
+ if (level == LogLevel::Error) {
+ std::cerr << line << std::endl;
+ } else {
+ std::cout << line << std::endl;
+ }
+}
+
+bool Logger::IsInitialized() {
+ return s_initialized;
+}
+
+const std::string& Logger::GetCurrentLogFile() {
+ return s_currentFile;
+}
diff --git a/DeviceConfigMonitorService/src/main.cpp b/DeviceConfigMonitorService/src/main.cpp
new file mode 100644
index 0000000..f1a0b52
--- /dev/null
+++ b/DeviceConfigMonitorService/src/main.cpp
@@ -0,0 +1,104 @@
+//============================================================================
+// DeviceConfigMonitorService - Main entry point
+//============================================================================
+
+#include
+#include
+#include
+#include
+#include
+
+#include "config.h"
+#include "logger.h"
+#include "heartbeat_worker.h"
+#include "service_main.h"
+
+#ifdef _WIN32
+#include
+#endif
+const std::string PROGRAM_NAME = "DeviceConfigMonitorService";
+const std::string VERSION = "1.0.0";
+
+// Console-mode-specific stop flag
+static std::atomic g_consoleStopRequested{false};
+
+// Ctrl+C signal handler: triggers graceful exit of the while loop
+static void ConsoleSignalHandler(int /*signum*/) {
+ g_consoleStopRequested.store(true);
+}
+
+void printBanner() {
+ std::cout << "========================================" << std::endl;
+ std::cout << " " << PROGRAM_NAME << std::endl;
+ std::cout << " Version: " << VERSION << std::endl;
+ std::cout << "========================================" << std::endl;
+}
+
+void printUsage(const char* programName) {
+ std::cout << "Usage: " << programName << " [--console]" << std::endl;
+ std::cout << " --console Run in console mode (with config + heartbeat)" << std::endl;
+ std::cout << " --help Show this help message" << std::endl;
+ std::cout << " (no flag) Run as Windows Service (registered via scripts/*.bat)" << std::endl;
+}
+
+int main(int argc, char* argv[]) {
+ bool consoleMode = false;
+
+ for (int i = 1; i < argc; i++) {
+ if (strcmp(argv[i], "--console") == 0) {
+ consoleMode = true;
+ }
+ else if (strcmp(argv[i], "--help") == 0) {
+ printUsage(argv[0]);
+ return 0;
+ }
+ }
+
+ if (consoleMode) {
+ printBanner();
+
+ // Register Ctrl+C handler
+ std::signal(SIGINT, ConsoleSignalHandler);
+ std::signal(SIGTERM, ConsoleSignalHandler);
+
+ // Print config summary (console-mode feature)
+ AppConfig previewConfig = LoadConfig();
+ PrintConfigSummary(previewConfig);
+ // Note: LoadConfig already reads config.json;
+ // RunServiceBody will read it again. That is fine (IO is lightweight).
+
+ std::cout << "Service is running. Press Ctrl+C to stop." << std::endl;
+ std::cout << std::endl;
+
+ // Shared business body
+ RunServiceBody(g_consoleStopRequested);
+
+ std::cout << "Service stopped gracefully." << std::endl;
+ }
+ else {
+ // Windows Service mode
+ // SERVICE_TABLE_ENTRY maps the service name to the ServiceMain entry point
+ // StartServiceCtrlDispatcher blocks until the service stops
+ // Note: In newer Windows SDK (10.0.19041+), SERVICE_TABLE_ENTRY
+ // has A and W variants, selected by the UNICODE macro.
+ // Our ServiceMain signature uses the wide-char version (LPWSTR*),
+ // so we explicitly use SERVICE_TABLE_ENTRYW for consistency.
+ SERVICE_TABLE_ENTRYW serviceTable[] = {
+ // L"..." is const, but lpServiceName is LPWSTR (non-const)
+ // SCM never modifies this string, so const_cast is safe
+ { const_cast(L"DeviceConfigMonitorService"), (LPSERVICE_MAIN_FUNCTIONW)ServiceMain },
+ { nullptr, nullptr }
+ };
+
+ if (!StartServiceCtrlDispatcherW(serviceTable)) {
+ // Start failed: possible causes include SCM being unavailable
+ // (e.g. running the exe directly instead of via services.msc)
+ // Write to stderr instead of calling Logger because Logger is not initialized yet
+ std::cerr << "StartServiceCtrlDispatcher failed. "
+ << "If you want to run in console mode, use --console flag." << std::endl;
+ return 1;
+ }
+ }
+
+ return 0;
+}
diff --git a/DeviceConfigMonitorService/src/service_main.cpp b/DeviceConfigMonitorService/src/service_main.cpp
new file mode 100644
index 0000000..d934944
--- /dev/null
+++ b/DeviceConfigMonitorService/src/service_main.cpp
@@ -0,0 +1,151 @@
+//============================================================================
+// DeviceConfigMonitorService - Windows Service implementation
+//============================================================================
+
+#include "service_main.h"
+
+#include
+#include
+#include
+#include
+
+#include "config.h"
+#include "logger.h"
+#include "heartbeat_worker.h"
+
+#ifdef _WIN32
+#include
+#endif
+
+//============================================================================
+// Global state (Windows Service must use global variables for ServiceStatusHandle)
+//============================================================================
+namespace {
+
+SERVICE_STATUS g_serviceStatus = {};
+SERVICE_STATUS_HANDLE g_serviceStatusHandle = nullptr;
+std::atomic g_serviceStopRequested{false};
+
+} // namespace
+
+//============================================================================
+// RunServiceBody - Shared business logic for console and service modes
+//============================================================================
+// Flow (same as --console in main.cpp):
+// 1) LoadConfig
+// 2) Logger::Init
+// 3) HeartbeatWorker.Start
+// 4) Main loop: check stopFlag every 200ms
+// 5) Graceful shutdown: worker.Stop() + Logger::Shutdown()
+//============================================================================
+void RunServiceBody(std::atomic& stopFlag) {
+ Logger::Info("Service starting up...");
+ AppConfig config = LoadConfig();
+ Logger::Info("Configuration loaded. DeviceId=" + config.DeviceId
+ + ", ServiceName=" + config.ServiceName
+ + ", LogPath=" + config.LogPath);
+
+ if (!Logger::Init(config.LogPath)) {
+ std::cerr << "[WARN] Logger init failed, console output only." << std::endl;
+ }
+ Logger::Info("Logger initialized. LogDir=" + config.LogPath);
+
+ HeartbeatWorker worker;
+ worker.Start(config);
+
+ Logger::Info("Service entered main loop.");
+ while (!stopFlag.load()) {
+ std::this_thread::sleep_for(std::chrono::milliseconds(200));
+ }
+
+ Logger::Info("Stop signal received. Shutting down...");
+ worker.Stop();
+ Logger::Info("Service stopped.");
+ Logger::Shutdown();
+}
+
+//============================================================================
+// ServiceMain - Entry point called by Windows SCM
+//============================================================================
+// Note: The signature of ServiceMain must exactly match
+// void WINAPI ServiceMain(DWORD argc, LPWSTR* argv)
+// It is called asynchronously by SCM; returning from this function does
+// NOT mean the service has stopped. We keep the service alive by calling
+// ReportStatus + running the business loop inside a worker thread.
+//============================================================================
+void WINAPI ServiceMain(DWORD /*argc*/, LPWSTR* /*argv*/) {
+ // 1) Register the control handler
+ g_serviceStatusHandle = RegisterServiceCtrlHandlerW(
+ L"DeviceConfigMonitorService",
+ ServiceCtrlHandler);
+
+ if (g_serviceStatusHandle == nullptr) {
+ // Registration failed: cannot continue (no handle means cannot report status)
+ return;
+ }
+
+ // 2) Initialize SERVICE_STATUS
+ g_serviceStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
+ g_serviceStatus.dwServiceSpecificExitCode = 0;
+ g_serviceStatus.dwWin32ExitCode = 0;
+ g_serviceStatus.dwWaitHint = 1000; // 1 second
+
+ // 3) Report SERVICE_START_PENDING
+ g_serviceStatus.dwCurrentState = SERVICE_START_PENDING;
+ g_serviceStatus.dwControlsAccepted = 0; // No control accepted during START_PENDING
+ SetServiceStatus(g_serviceStatusHandle, &g_serviceStatus);
+
+ // 4) Start the business thread
+ // Note: ServiceMain itself is a callback invoked by SCM and must not block;
+ // business logic must run in a child thread; ServiceMain returns promptly.
+ std::thread serviceThread([&]() {
+ // Report RUNNING from the child thread
+ g_serviceStatus.dwCurrentState = SERVICE_RUNNING;
+ g_serviceStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP
+ | SERVICE_ACCEPT_SHUTDOWN;
+ SetServiceStatus(g_serviceStatusHandle, &g_serviceStatus);
+
+ // Business body (blocks until stopFlag is set by CtrlHandler)
+ RunServiceBody(g_serviceStopRequested);
+
+ // Report SERVICE_STOPPED after the business loop exits
+ g_serviceStatus.dwCurrentState = SERVICE_STOPPED;
+ g_serviceStatus.dwControlsAccepted = 0;
+ SetServiceStatus(g_serviceStatusHandle, &g_serviceStatus);
+ });
+
+ // 5) ServiceMain returns promptly
+ // Business logic runs in serviceThread
+ if (serviceThread.joinable()) {
+ serviceThread.join();
+ }
+}
+
+//============================================================================
+// ServiceCtrlHandler - Receives control codes
+//============================================================================
+// Currently handles STOP and SHUTDOWN only; other codes return immediately.
+//============================================================================
+void WINAPI ServiceCtrlHandler(DWORD ctrlCode) {
+ switch (ctrlCode) {
+ case SERVICE_CONTROL_STOP:
+ // 1) Report STOP_PENDING first
+ g_serviceStatus.dwCurrentState = SERVICE_STOP_PENDING;
+ SetServiceStatus(g_serviceStatusHandle, &g_serviceStatus);
+
+ // 2) Set the stop flag so the business loop exits
+ g_serviceStopRequested.store(true);
+ break;
+
+ case SERVICE_CONTROL_SHUTDOWN:
+ // System shutdown: also exit gracefully
+ g_serviceStatus.dwCurrentState = SERVICE_STOP_PENDING;
+ SetServiceStatus(g_serviceStatusHandle, &g_serviceStatus);
+ g_serviceStopRequested.store(true);
+ break;
+
+ default:
+ // Other control codes: return immediately
+ break;
+ }
+}
diff --git a/DeviceConfigMonitorService/third_party/nlohmann/json.hpp b/DeviceConfigMonitorService/third_party/nlohmann/json.hpp
new file mode 100644
index 0000000..82d69f7
--- /dev/null
+++ b/DeviceConfigMonitorService/third_party/nlohmann/json.hpp
@@ -0,0 +1,25526 @@
+// __ _____ _____ _____
+// __| | __| | | | JSON for Modern C++
+// | | |__ | | | | | | version 3.12.0
+// |_____|_____|_____|_|___| https://github.com/nlohmann/json
+//
+// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann
+// SPDX-License-Identifier: MIT
+
+/****************************************************************************\
+ * Note on documentation: The source files contain links to the online *
+ * documentation of the public API at https://json.nlohmann.me. This URL *
+ * contains the most recent documentation and should also be applicable to *
+ * previous versions; documentation for deprecated functions is not *
+ * removed, but marked deprecated. See "Generate documentation" section in *
+ * file docs/README.md. *
+\****************************************************************************/
+
+#ifndef INCLUDE_NLOHMANN_JSON_HPP_
+#define INCLUDE_NLOHMANN_JSON_HPP_
+
+#include // all_of, find, for_each
+#include // nullptr_t, ptrdiff_t, size_t
+#include // hash, less
+#include // initializer_list
+#ifndef JSON_NO_IO
+ #include // istream, ostream
+#endif // JSON_NO_IO
+#include // random_access_iterator_tag
+#include // unique_ptr
+#include // string, stoi, to_string
+#include // declval, forward, move, pair, swap
+#include // vector
+
+// #include
+// __ _____ _____ _____
+// __| | __| | | | JSON for Modern C++
+// | | |__ | | | | | | version 3.12.0
+// |_____|_____|_____|_|___| https://github.com/nlohmann/json
+//
+// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann
+// SPDX-License-Identifier: MIT
+
+
+
+#include
+
+// #include
+// __ _____ _____ _____
+// __| | __| | | | JSON for Modern C++
+// | | |__ | | | | | | version 3.12.0
+// |_____|_____|_____|_|___| https://github.com/nlohmann/json
+//
+// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann
+// 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
+
+// #include
+// __ _____ _____ _____
+// __| | __| | | | JSON for Modern C++
+// | | |__ | | | | | | version 3.12.0
+// |_____|_____|_____|_|___| https://github.com/nlohmann/json
+//
+// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann
+// SPDX-License-Identifier: MIT
+
+
+
+#include // transform
+#include // array
+#include // forward_list
+#include // inserter, front_inserter, end
+#include