aiDgePLC 完整使用文档
aiDgePLC --- 基于 STVM 的 IEC 61131-3 ST 调试与运行环境
提供交互式 CLI 调试器、WebSocket 调试服务器,支持源码行号断点、观察点(Watchpoint)、
单步执行、变量查看、调用栈追踪,基于策略模式实现可扩展的命令分发。
对应虚拟机开源地址:https://gitee.com/galaxy_0/stvm
目录
- 快速开始
- 构建指南
- [CLI 调试器详解](#CLI 调试器详解)
- [WebSocket 调试服务器](#WebSocket 调试服务器)
- 策略模式命令分发架构
- [在项目中集成 Debugger](#在项目中集成 Debugger)
- FAQ
1. 快速开始
1.1 环境要求
- OS:Windows 10/11(开发)+ WSL2 Ubuntu(构建)
- 编译器:GCC 9+ 或 Clang 10+(支持 C++17)
- CMake:≥ 3.15
- 交叉编译(可选):Linaro aarch64-none-linux-gnu-gcc 9.x
1.2 一键构建
bash
# 在 WSL 中执行
cd /mnt/e/work/dev/aiDgePLC
bash build.sh
构建产物:
| 文件 | 说明 |
|---|---|
build/aidgeplc-debug |
CLI 交互式调试器 |
build/aidgeplc-debug-server |
WebSocket 调试服务器 |
build/tests/test_debugger |
单元测试 |
1.3 三十秒体验
bash
# 编写一个 IEC ST 程序
cat > /tmp/hello.iec << 'EOF'
PROGRAM main
VAR
counter : INT := 0;
sum : INT := 0;
END_VAR
FOR counter := 1 TO 10 DO
sum := sum + counter;
END_FOR;
END_PROGRAM
EOF
# 启动调试器
./build/aidgeplc-debug /tmp/hello.iec
# 在调试器中:
# break 6:L ← 在第6行设置断点
# run ← 运行到断点
# print sum ← 查看变量
# watch sum ← 添加观察点
# run ← 继续,观察点触发时暂停
# info watchpoints ← 查看观察点
# quit
2. 构建指南
2.1 构建脚本
bash
# 本地构建(Release)
./build.sh
# 指定构建类型
./build.sh Debug
./build.sh RelWithDebInfo
# 交叉编译(aarch64)
./build.sh Release aarch64
2.2 CMake 选项
| 选项 | 默认值 | 说明 |
|---|---|---|
AIDEGEPLC_BUILD_TOOLS |
ON | 构建 CLI 工具 aidgeplc-debug |
AIDEGEPLC_BUILD_TESTS |
ON | 构建单元测试(交叉编译自动关闭) |
AIDEGEPLC_BUILD_DEBUG_SERVER |
ON | 构建 WebSocket 调试服务器 |
AIDEGEPLC_USE_LIBCO |
OFF | 透传给 stvm,启用 libco 协程 |
AIDEGEPLC_USE_CPP_MISC_FETCH |
ON | 从 Gitee 自动获取 cpp-misc |
CPP_MISC_ROOT |
空 | 离线环境:指向本地 cpp-misc 根目录 |
AIDEGEPLC_USE_LIBHV_FETCH |
ON | 从 Gitee 自动获取 libhv |
LIBHV_ROOT |
空 | 离线环境:指向本地 libhv 根目录 |
CMAKE_TOOLCHAIN_FILE |
空 | 指定交叉编译 toolchain |
2.3 手动 CMake 构建
bash
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
cmake --build . --parallel $(nproc)
2.4 外部依赖
| 依赖 | 来源 | 集成方式 |
|---|---|---|
| stvm | gitee.com/galaxy_0/stvm | Git 子模块(兄弟目录 ./stvm) |
| cpp-misc | gitee.com/galaxy_0/cpp-misc | FetchContent 或 CPP_MISC_ROOT |
| libhv | gitee.com/galaxy_0/libhv | FetchContent 或 LIBHV_ROOT |
3. CLI 调试器详解
3.1 启动方式
bash
# 启动时加载源文件
aidgeplc-debug <source.iec|source.st>
# 空壳启动,后续用 load 命令加载
aidgeplc-debug
3.2 命令一览
程序加载
| 命令 | 说明 |
|---|---|
load <file> |
从文件加载 IEC ST 源码,自动编译到内存 |
compile "PROGRAM main ... END_PROGRAM" |
内联编译 ST 代码 |
reset |
重置执行状态(保留程序和断点) |
执行控制
| 命令 | 说明 |
|---|---|
run [N] / continue [N] |
运行直到断点/观察点/结束,可选最大指令数 N |
step / s |
单步进入(step into) |
next / n |
单步步过(step over) |
finish |
步出当前 POU(step out) |
断点(按 IP 地址 / 按源码行号)
| 命令 | 说明 |
|---|---|
break <ip> |
在字节码 IP 地址设断点 |
break <line>:L |
按源码行号设断点(自动映射到字节码地址) |
clear <ip> |
清除 IP 断点 |
clear <line>:L |
清除行号断点 |
enable <ip> |
启用断点 |
disable <ip> |
禁用断点 |
info breakpoints |
列出所有断点 |
行号断点原理 :stvm 在生成字节码时同步记录每条指令对应的源码行号(
line_table)。
break <line>:L通过line_table将行号映射到一个或多个字节码地址,在这些地址上设置断点。
观察点(Watchpoint)
| 命令 | 说明 |
|---|---|
watch <varname> |
添加观察点(变量必须已声明) |
unwatch <varname> |
移除观察点 |
info watchpoints |
列出所有观察点 |
观察点原理 :每条指令执行后,调试器检查所有启用的观察点变量的当前值与上次记录值。
若值发生变化,自动暂停执行并记录触发变量名。适用于监控状态机切换、计数器变化等场景。
数据查看
| 命令 | 说明 |
|---|---|
vars / info variables |
打印所有全局变量 |
print <name> / p <name> |
打印单个变量值 |
set <name>=<value> |
修改变量值 |
registers / info registers |
打印 IP / SP / BP / Clock |
stack |
打印操作数栈 |
调用栈 / 反汇编
| 命令 | 说明 |
|---|---|
bt / backtrace |
打印调用栈 |
list / disasm |
反汇编当前 IP 上下文 |
disasm <start> <end> |
反汇编指定范围 |
state |
打印调试器状态 |
其他
| 命令 | 说明 |
|---|---|
help |
显示帮助 |
quit / exit |
退出 |
3.3 命令缩写
所有命令支持前缀匹配。例如:
b→breakr→runp→printi b→info breakpointsi w→info watchpoints
当多个命令匹配同一前缀时,调试器会提示歧义。
3.4 调试会话示例
$ aidgeplc-debug /tmp/counter.iec
aiDgePLC 交互式调试器 [策略模式 cpp-misc]
已加载: /tmp/counter.iec
入口 IP = 0, 代码大小 = 120 字节, 符号 2 个
[aidgeplc-dbg | LOADED | ip=0] break 6:L
断点已设置 @ 行 6 (映射到 2 个 IP: 48 54)
[aidgeplc-dbg | LOADED | ip=0] run
执行结果: PAUSED (断点/限制); IP=48
[aidgeplc-dbg | PAUSED | ip=48] print counter
counter = 1
[aidgeplc-dbg | PAUSED | ip=48] watch sum
观察点已添加: sum
[aidgeplc-dbg | PAUSED | ip=48] run
执行结果: PAUSED (断点/限制); IP=48
[aidgeplc-dbg | PAUSED | ip=48] print sum
sum = 1
[aidgeplc-dbg | PAUSED | ip=48] info watchpoints
观察点列表 (1 个):
sum enabled=yes hits=1 last=1
[aidgeplc-dbg | PAUSED | ip=48] bt
调用栈 (深度 1):
#0 ip=48 bp=0 pou=0
[aidgeplc-dbg | PAUSED | ip=48] quit
再见。
4. WebSocket 调试服务器
4.1 概述
aidgeplc-debug-server 是基于 libhv WebSocket 模块的调试协议服务器,兼容 OpenPLC Runtime v4 调试协议,并扩展了完整的调试器控制功能码。
架构:
┌──────────────┐ WebSocket ┌──────────────────┐
│ 调试客户端 │ ←─────────────────→ │ DebugServer │
│ (浏览器/脚本) │ ws://host:port │ │
└──────────────┘ │ ┌────────────┐ │
│ │ strategy │ │ ← 按功能码分发
│ │ 分发器 │ │
│ └─────┬──────┘ │
│ │ │
│ ┌─────▼──────┐ │
│ │ Debugger │ │ ← stvm VM/Compiler
│ └────────────┘ │
└──────────────────┘
4.2 启动
bash
# 默认监听 0.0.0.0:8443
./build/aidgeplc-debug-server
# 指定端口和地址
./build/aidgeplc-debug-server --port 9000 --host 127.0.0.1
# 启动时自动加载源文件
./build/aidgeplc-debug-server --load /tmp/hello.iec
WebSocket 端点 :ws://<host>:<port>/api/debug
4.3 协议格式
传输层:WebSocket 文本帧
报文格式:空格分隔的十六进制字符串
请求示例: "44 00 03 00 00 00 01 00 02"
^^ ^^^^^^
功能码 数据
响应前缀:
0x7E(~)= 成功0x7F= 错误(aiDgePLC 扩展),后跟[err_len:2][err_msg]
4.4 功能码详解
核心功能码(兼容 OpenPLC v4)
0x41 DEBUG_INFO --- 获取程序信息
请求: 41
响应: 7E [entry_ip:4][code_size:4][symbol_count:4]
([name_len:2][name][offset:4][type:1][array_size:4]) × symbol_count
0x42 DEBUG_SET --- 设置变量跟踪标志
请求: 42 [name_len:2][name][flags:1]
响应: 7E
flags的 bit 0 设置时,将该变量映射为观察点(Watchpoint)。
0x43 DEBUG_GET --- 获取单个变量值
请求: 43 [name_len:2][name]
响应: 7E [value bytes][type_code:1]
变量值编码:
| type_code | 类型 | 值字节 |
|---|---|---|
| 0x01 | BOOL | 1 字节 |
| 0x02 | INT | 8 字节(int64 小端) |
| 0x03 | REAL | 8 字节(double 小端) |
| 0x04 | TIME | 8 字节(int64 微秒,小端) |
| 0x05 | STR | 2 字节长度 + 内容 |
| 0x00 | UNKNOWN | 无值字节 |
0x44 DEBUG_GET_LIST --- 批量获取变量值
请求: 44 [count:2]([name_len:2][name])...
响应: 7E [count:2]([value bytes][type_code:1])...
0x45 DEBUG_GET_MD5 --- 获取程序 MD5
请求: 45 [padding...]
响应: 7E [32字节十六进制MD5字符串]\0
aiDgePLC 扩展功能码
0x46 DEBUG_LOAD --- 载入 IEC 源文本
请求: 46 [src_len:4][src ASCII]
响应: 7E ← 成功
7F [err_len:2][err_msg] ← 编译失败
示例 :载入 PROGRAM main VAR x:INT:=0; END_VAR x:=42; END_PROGRAM
46 00 00 00 2F 50 52 4F 47 52 41 4D 20 6D 61 69 6E ...
└──src_len=47──┘ └── "PROGRAM main..." ASCII ──────┘
0x47 DEBUG_RUN --- 运行
请求: 47 [max_ins:8] (max_ins=0 表示无限制)
响应: 7E [state:1][ip:4]
状态码:
| 值 | 状态 |
|---|---|
| 0 | NOT_LOADED |
| 1 | LOADED |
| 2 | RUNNING |
| 3 | PAUSED |
| 4 | FINISHED |
| 5 | ERROR |
0x48 DEBUG_STEP --- 单步执行
请求: 48 [mode:1] (0=into, 1=over, 2=out)
响应: 7E [state:1][ip:4]
0x49 DEBUG_BRK_SET --- 设置断点
请求: 49 [type:1][value:4]
type=0: value = IP 地址
type=1: value = 源码行号
响应: 7E [count:4] (映射到的断点数量)
0x4A DEBUG_BRK_CLR --- 清除断点
请求: 4A [type:1][value:4]
响应: 7E [count:4] (清除的断点数量)
0x4B DEBUG_BRK_LIST --- 列出断点
请求: 4B
响应: 7E [count:4]([ip:4][enabled:1][hits:8])...
0x4C DEBUG_STATE --- 查询状态
请求: 4C
响应: 7E [state:1][ip:4][sp:4][bp:4]
0x4D DEBUG_RESET --- 重置执行
请求: 4D
响应: 7E
0x4E DEBUG_CALLSTACK --- 查询调用栈
请求: 4E
响应: 7E [depth:4]([ip:4][bp:4][pou:4])...
0x4F DEBUG_DISASM --- 反汇编
请求: 4F [start:4][end:4]
响应: 7E [hex ASCII...] (反汇编文本的十六进制编码)
4.5 协议交互示例
以下用 Python 演示完整的调试会话:
python
import asyncio
import websockets
async def debug_session():
uri = "ws://127.0.0.1:8443/api/debug"
async with websockets.connect(uri) as ws:
# 1. 载入程序
src = b"PROGRAM main VAR x:INT:=0; END_VAR x:=1; x:=2; x:=3; END_PROGRAM"
req = bytes([0x46]) + len(src).to_bytes(4, 'little') + src
await ws.send(' '.join(f'{b:02X}' for b in req))
resp = await ws.recv()
print(f"LOAD: {resp}") # 7E
# 2. 在第4行设断点 (x:=2)
req = bytes([0x49, 0x01]) + (4).to_bytes(4, 'little')
await ws.send(' '.join(f'{b:02X}' for b in req))
resp = await ws.recv()
print(f"BRK_SET: {resp}") # 7E 02 00 00 00 (2个断点)
# 3. 运行
req = bytes([0x47]) + (0).to_bytes(8, 'little')
await ws.send(' '.join(f'{b:02X}' for b in req))
resp = await ws.recv()
print(f"RUN: {resp}") # 7E 03 xx xx xx xx (PAUSED)
# 4. 获取变量 x
name = b"x"
req = bytes([0x43]) + len(name).to_bytes(2, 'little') + name
await ws.send(' '.join(f'{b:02X}' for b in req))
resp = await ws.recv()
print(f"GET x: {resp}") # 7E 02 00 00 00 00 00 00 00 02
asyncio.run(debug_session())
4.6 并发安全
单个 Debugger 实例被所有 WebSocket 连接共享。命令通过 std::mutex 串行化执行,
避免并发破坏 VM 执行状态。多客户端连接时,命令按到达顺序排队处理。
5. 策略模式命令分发架构
5.1 设计动机
aiDgePLC 的 CLI 调试器和 WebSocket 调试服务器都需要命令分发功能:
- CLI:解析用户输入的命令字符串,调用对应处理函数
- 服务器:解析协议功能码,调用对应处理器
传统做法是用 if-else / switch-case 链,但存在以下问题:
- 命令数量增长后代码臃肿,难以维护
- 新增命令需要修改分发函数,违反开闭原则
- 无法在运行时动态注册/注销命令
aiDgePLC 采用 cpp-misc 的
wheels::dm::strategy 模板类实现策略模式命令分发,解决上述问题。
5.2 strategy 模板类
cpp
// 头文件: cpp-misc/include/designM/strategy.hpp
namespace wheels::dm {
template <typename Key, typename Signature>
class strategy;
// 特化:Key=std::string, Signature=void(const Context&)
template <>
class strategy<std::string, void(const Context&)> {
public:
// 注册策略
void add(const std::string& key, std::function<void(const Context&)> fn);
// 精确匹配执行
bool run(const std::string& key, const Context& ctx);
// 获取所有已注册的 key
std::vector<std::string> keys() const;
};
} // namespace wheels::dm
5.3 CLI 命令分发实现
5.3.1 定义命令上下文
cpp
struct CliContext {
Debugger& dbg; // 调试器引用
bool& running; // 控制主循环退出
const std::string& line; // 原始输入行
const std::vector<std::string>& args; // 切分后的参数
};
5.3.2 命令处理函数
每个命令对应一个处理函数,签名为 void(const CliContext&):
cpp
static void cmdRun_impl(const CliContext& c) {
uint64_t max_ins = 0;
if (c.args.size() > 1) max_ins = std::stoull(c.args[1]);
auto state = c.dbg.run(max_ins);
std::printf("执行结果: %s", stateName(state));
// ...
}
static void cmdBreak_impl(const CliContext& c) {
if (c.args.size() < 2) { std::printf("用法: break <ip|line:L>\n"); return; }
bool is_line; uint32_t val;
if (!parseBreakArg(c.args[1], is_line, val)) { std::printf("无效参数\n"); return; }
if (is_line) {
uint32_t n = c.dbg.setBreakpointByLine(val);
std::printf("断点已设置 @ 行 %u (映射到 %u 个 IP)\n", val, n);
} else {
c.dbg.setBreakpoint(val);
std::printf("断点已设置 @ IP %u\n", val);
}
}
5.3.3 策略表注册
cpp
class CommandDispatcher {
using StrategyT = wheels::dm::strategy<std::string, void(const CliContext&)>;
StrategyT strategy_;
public:
CommandDispatcher() {
registerCmd("help", cmdHelp_impl);
registerCmd("quit", cmdQuit_impl);
registerCmd("load", cmdLoad_impl);
registerCmd("run", cmdRun_impl);
registerCmd("break", cmdBreak_impl);
registerCmd("watch", cmdWatch_impl);
registerCmd("print", cmdPrint_impl);
registerCmd("bt", cmdBacktrace_impl);
// ... 共 30+ 条命令
}
void registerCmd(const std::string& name, CmdFn fn) {
strategy_.add(name, fn);
}
};
5.3.4 主循环分发
cpp
void CommandDispatcher::dispatch(const std::string& input, CliContext& ctx) {
auto args = splitArgs(input);
if (args.empty()) return;
std::string cmd = args[0];
// 1. 精确匹配
if (strategy_.run(cmd, ctx)) return;
// 2. 前缀匹配(支持缩写)
std::vector<std::string> matches;
for (const auto& key : strategy_.keys()) {
if (cmdMatch(cmd, key)) matches.push_back(key);
}
if (matches.size() == 1) {
strategy_.run(matches[0], ctx); // 唯一匹配,执行
} else if (matches.size() > 1) {
std::printf("歧义命令 '%s',匹配: %s\n",
cmd.c_str(), join(matches).c_str());
} else {
std::printf("未知命令: %s (输入 help 查看命令列表)\n", cmd.c_str());
}
}
5.4 服务器命令分发实现
调试服务器使用相同策略,但以功能码为 key:
cpp
void DebugServer::registerHandlers() {
// key = 功能码的 2 位十六进制字符串
strategy_.add("41", [this](const Bytes& req){ return handleInfo(req); });
strategy_.add("42", [this](const Bytes& req){ return handleSet(req); });
strategy_.add("43", [this](const Bytes& req){ return handleGet(req); });
strategy_.add("44", [this](const Bytes& req){ return handleGetList(req); });
strategy_.add("45", [this](const Bytes& req){ return handleGetMd5(req); });
strategy_.add("46", [this](const Bytes& req){ return handleLoad(req); });
strategy_.add("47", [this](const Bytes& req){ return handleRun(req); });
strategy_.add("48", [this](const Bytes& req){ return handleStep(req); });
strategy_.add("49", [this](const Bytes& req){ return handleBrkSet(req); });
strategy_.add("4A", [this](const Bytes& req){ return handleBrkClr(req); });
strategy_.add("4B", [this](const Bytes& req){ return handleBrkList(req); });
strategy_.add("4C", [this](const Bytes& req){ return handleState(req); });
strategy_.add("4D", [this](const Bytes& req){ return handleReset(req); });
strategy_.add("4E", [this](const Bytes& req){ return handleCallstack(req); });
strategy_.add("4F", [this](const Bytes& req){ return handleDisasm(req); });
}
std::vector<uint8_t> DebugServer::handleCommand(const std::vector<uint8_t>& req) {
if (req.empty()) return makeErr("empty request");
// 功能码 -> 2 位十六进制 key
char hex[3];
std::snprintf(hex, sizeof(hex), "%02X", req[0]);
std::lock_guard<std::mutex> lk(mtx_);
std::vector<uint8_t> resp;
if (!strategy_.run(hex, req, resp)) {
return makeErr("unknown function code: " + std::string(hex));
}
return resp;
}
5.5 策略模式的优势
| 特性 | if-else 链 | strategy 模式 |
|---|---|---|
| 新增命令 | 修改分发函数 | registerCmd() 一行注册 |
| 命令列表 | 手动维护 | strategy_.keys() 自动获取 |
| 运行时注册 | 不支持 | 支持(动态增删) |
| 代码组织 | 集中臃肿 | 分散在各处理函数 |
| 可测试性 | 需模拟整个分发器 | 各策略函数独立测试 |
| 前缀匹配 | 手动实现 | 遍历 keys() 即可 |
5.6 Fallback 机制
当 cpp-misc 不可用时(离线环境未设置 CPP_MISC_ROOT),系统自动降级为
std::map<std::string, std::function> 实现,接口完全一致:
cpp
#ifdef AIDEGEPLC_USE_STRATEGY
wheels::dm::strategy<std::string, void(const CliContext&)> strategy_;
#else
std::map<std::string, CmdFn> strategy_;
#endif
通过 AIDEGEPLC_USE_STRATEGY / AIDEGEPLC_USE_SRV_STRATEGY 宏切换,
确保在无 cpp-misc 环境下仍可编译运行。
6. 在项目中集成 Debugger
6.1 CMake 集成
cmake
# 引入 aiDgePLC 库
find_package(aiDgePLC REQUIRED)
target_link_libraries(my_app PRIVATE aidgeplc)
6.2 C++ API 使用
cpp
#include "aidgeplc/debugger.hpp"
#include <iostream>
int main() {
aidgeplc::Debugger dbg;
std::string err;
// 1. 载入 IEC ST 源码(自动编译到内存)
if (!dbg.loadIecSource(R"IEC(
PROGRAM main
VAR counter : INT := 0; END_VAR
FOR counter := 1 TO 100 DO
counter := counter; // 占位
END_FOR;
END_PROGRAM
)IEC", err)) {
std::cerr << "编译失败: " << err << std::endl;
return 1;
}
// 2. 按源码行号设置断点
dbg.setBreakpointByLine(5); // 第 5 行
// 3. 添加观察点
dbg.addWatchpoint("counter");
// 4. 运行到断点
auto state = dbg.run();
if (state == aidgeplc::DebugState::PAUSED) {
// 5. 读取变量
stvm::StackValue v;
dbg.readVariable("counter", v);
std::cout << "counter = " << v.toInt() << std::endl;
// 6. 修改变量
dbg.writeVariable("counter", stvm::StackValue::fromInt(50));
// 7. 单步
dbg.step(aidgeplc::StepMode::INTO);
// 8. 查看调用栈
auto frames = dbg.getCallStack();
for (auto& f : frames) {
std::cout << " #" << f.frame_index
<< " ip=" << f.return_ip
<< " pou=" << f.pou_index << std::endl;
}
// 9. 继续运行
dbg.run();
}
return 0;
}
6.3 DebugServer API 使用
cpp
#include "aidgeplc/debug_server.hpp"
int main() {
aidgeplc::DebugServer server;
server.setPort(8443);
server.setHost("0.0.0.0");
server.setSourceFile("/opt/plc/program.iec"); // 可选:启动时预加载
return server.run(); // 阻塞直到 stop()
}
6.4 核心 API 速查
Debugger
| 方法 | 说明 |
|---|---|
loadIecSource(src, err) |
载入 ST 源码,自动编译 |
loadSourceFile(path, err) |
从文件载入 |
run(max_ins) |
运行(0=无限制) |
step(mode) |
单步(INTO/OVER/OUT) |
reset() |
重置执行状态 |
setBreakpoint(ip) |
按 IP 设断点 |
setBreakpointByLine(line) |
按行号设断点,返回映射数量 |
clearBreakpoint(ip) |
清除断点 |
addWatchpoint(name) |
添加观察点 |
removeWatchpoint(name) |
移除观察点 |
readVariable(name, v) |
读取变量值 |
writeVariable(name, v) |
写入变量值 |
dumpVariables() |
打印所有变量 |
getCallStack() |
获取调用栈 |
dumpRegisters() |
打印寄存器 |
dumpStack() |
打印操作数栈 |
disassembleCurrent() |
反汇编当前上下文 |
state() |
获取调试器状态 |
currentIP() |
获取当前 IP |
DebugServer
| 方法 | 说明 |
|---|---|
setPort(port) |
设置监听端口 |
setHost(host) |
设置监听地址 |
setSourceFile(path) |
设置预加载源文件 |
run() |
启动服务器(阻塞) |
stop() |
停止服务器 |
handleCommand(req) |
处理单条协议命令(供测试) |
7. FAQ
Q: 如何在离线环境构建?
设置 CPP_MISC_ROOT 和 LIBHV_ROOT 指向本地副本:
bash
cmake .. \
-DCPP_MISC_ROOT=/path/to/cpp-misc \
-DLIBHV_ROOT=/path/to/libhv
Q: 交叉编译时调试服务器不可用?
libhv 的 add_subdirectory 在交叉编译时可能因依赖问题失败。
调试服务器会自动跳过构建,CLI 调试器不受影响。
Q: 行号断点映射到 0 个 IP?
该行可能没有生成任何字节码(如注释行、空行、声明行)。请尝试在赋值语句
或控制流语句所在行设置断点。
Q: 观察点没有触发?
观察点在每条指令执行后 检查。如果变量在断点暂停期间被修改(通过 set 命令),
需要在下次 run 时才会检测到变化。refreshWatchpoints() 可手动重置基线值。
Q: WebSocket 消息格式为什么用空格分隔的十六进制?
兼容 OpenPLC Runtime v4 协议。原始协议使用文本帧传输十六进制字符串,
便于调试和日志记录。hexToBytes() / bytesToHex() 工具函数处理转换。
Q: 如何扩展新的协议功能码?
- 在
debug_server.hpp的proto命名空间添加功能码常量 - 在
DebugServer类添加handleXxx处理方法 - 在
registerHandlers()中注册:strategy_.add("5x", [this](const Bytes& r){ return handleXxx(r); })
无需修改 handleCommand() 分发逻辑------策略模式自动路由。