构建:修复 GTest 链接失败——显式指定 Linux GTest 配置,避免从 PATH 探测到 Windows Anaconda 的 .lib

This commit is contained in:
zjk
2026-08-18 19:45:50 +08:00
parent a6da5e3ae9
commit f3dd9e2c5a
20 changed files with 4629 additions and 6 deletions
+1
View File
@@ -250,6 +250,7 @@ bin/ccuState.txt
bin/pPowerManger bin/pPowerManger
bin/SQLiteTest bin/SQLiteTest
bin/pPMtest bin/pPMtest
bin/pPowerMangerHost
# CI test reports # CI test reports
reports/ reports/
+18 -1
View File
@@ -38,7 +38,24 @@ done
mkdir -p build mkdir -p build
cd build cd build
cmake -DCMAKE_BUILD_TYPE=${BUILD_TYPE} ../ # 修复: PATH 中包含了 Windows Anaconda(/mnt/d/Software/Anaconda),
# 其下的 GTest 是 Windows .lib, 无法被 Linux 链接器使用。
# 因此显式指定 Linux 平台的 GTest 配置, 覆盖 CMake 从 PATH 探测到的不正确路径。
CMAKE_ARGS="-DCMAKE_BUILD_TYPE=${BUILD_TYPE}"
if [ -z "${GTest_DIR}" ]; then
for GTEST_CONFIG in \
/usr/lib/x86_64-linux-gnu/cmake/GTest/GTestConfig.cmake \
/usr/lib/cmake/GTest/GTestConfig.cmake \
/usr/local/lib/cmake/GTest/GTestConfig.cmake \
"${HOME}/miniconda/lib/cmake/GTest/GTestConfig.cmake"; do
if [ -f "${GTEST_CONFIG}" ]; then
CMAKE_ARGS="${CMAKE_ARGS} -DGTest_DIR=$(dirname "${GTEST_CONFIG}")"
break
fi
done
fi
cmake ${CMAKE_ARGS} ../
make ${CMD_LINE_ARGS} make ${CMD_LINE_ARGS}
cd ${INVOCATION_ABS_DIR} cd ${INVOCATION_ABS_DIR}
File diff suppressed because it is too large Load Diff
+306
View File
@@ -0,0 +1,306 @@
#!/bin/bash
#=======================================================================
# FILE: scripts/run_host_sim.sh
# DESC: 运行与测试 pPowerMangerHost(模拟 pPowerManger 上位机的网页程序)
#
# 用法:
# ./scripts/run_host_sim.sh run 启动 MOOSDB + pPowerManger
# + pPowerMangerHost(前台运行,
# Ctrl-C 退出并清理)
# ./scripts/run_host_sim.sh test 自动化测试 pPowerMangerHost:
# 网页 HTTP / WebSocket 反馈 /
# 指令下发到 MOOSDB
# ./scripts/run_host_sim.sh build 仅编译需要的目标
# ./scripts/run_host_sim.sh stop 清理残留进程
# ./scripts/run_host_sim.sh help 显示帮助
#
# 环境变量(可覆盖):
# SERVER_HOST MOOSDB 地址(默认 localhost)
# SERVER_PORT MOOSDB 端口(默认 9000)
# WEB_PORT 网页监听端口(默认 18080)
# WITH_POWERMANGER run 模式是否同时启动 pPowerManger(默认 1)
#=======================================================================
set -uo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
BIN_DIR="${ROOT}/bin"
MOOS_TEMPLATE="${ROOT}/src/pPowerMangerHost/pPowerMangerHost.moos"
SERVER_HOST="${SERVER_HOST:-localhost}"
SERVER_PORT="${SERVER_PORT:-9000}"
WEB_PORT="${WEB_PORT:-18080}"
WITH_POWERMANGER="${WITH_POWERMANGER:-1}"
MOOSDB_BIN="${MOOSDB_BIN:-$(command -v MOOSDB || true)}"
POKE_BIN="$(command -v uPokeDB || true)"
PM_BIN="${BIN_DIR}/pPowerManger"
HOST_BIN="${BIN_DIR}/pPowerMangerHost"
MOOSDB_PID=""
PM_PID=""
HOST_PID=""
WORKDIR=""
MISSION=""
info() { printf '\033[1;36m[host-sim]\033[0m %s\n' "$*"; }
ok() { printf '\033[1;32m[host-sim]\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m[host-sim]\033[0m %s\n' "$*"; }
err() { printf '\033[1;31m[host-sim]\033[0m %s\n' "$*"; }
usage() {
sed -n '2,23p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
exit 0
}
# 生成临时 mission 文件(同时含 pPowerManger 与 pPowerMangerHost 两个配置块)
make_mission() {
WORKDIR="$(mktemp -d)"
MISSION="${WORKDIR}/host_sim.moos"
cat > "${MISSION}" <<EOF
ServerHost = ${SERVER_HOST}
ServerPort = ${SERVER_PORT}
Community = h100hostsim
ProcessConfig = pPowerManger
{
AppTick = 4
CommsTick = 4
}
ProcessConfig = pPowerMangerHost
{
AppTick = 4
CommsTick = 4
WEB_PORT = ${WEB_PORT}
}
EOF
}
kill_proc() {
[ -n "$1" ] && kill "$1" 2>/dev/null && wait "$1" 2>/dev/null
}
cleanup() {
kill_proc "${HOST_PID}"
kill_proc "${PM_PID}"
kill_proc "${MOOSDB_PID}"
[ -n "${WORKDIR}" ] && rm -rf "${WORKDIR}"
}
build_targets() {
info "编译 pPowerManger / pPowerMangerHost ..."
mkdir -p "${ROOT}/build"
cd "${ROOT}/build"
cmake -DCMAKE_BUILD_TYPE=Release "${ROOT}" >/dev/null 2>&1 || { err "cmake 失败"; return 1; }
make -j2 pPowerManger pPowerMangerHost >/dev/null 2>&1 || { err "make 失败"; return 1; }
cd "${ROOT}"
ok "编译完成"
}
check_binaries() {
local missing=0
[ -n "${MOOSDB_BIN}" ] || { err "未找到 MOOSDB(请加入 PATH)"; missing=1; }
[ -x "${HOST_BIN}" ] || { err "未找到 ${HOST_BIN}(请先运行: $0 build)"; missing=1; }
if [ "${WITH_POWERMANGER}" = "1" ]; then
[ -x "${PM_BIN}" ] || { err "未找到 ${PM_BIN}(请先运行: $0 build)"; missing=1; }
fi
return "${missing}"
}
start_moosdb() {
make_mission
info "启动 MOOSDB (${SERVER_HOST}:${SERVER_PORT}) ..."
"${MOOSDB_BIN}" --moos_no_colour "${MISSION}" > "${WORKDIR}/moosdb.log" 2>&1 &
MOOSDB_PID=$!
sleep 1
kill -0 "${MOOSDB_PID}" 2>/dev/null || { err "MOOSDB 未存活"; return 1; }
}
start_pm() {
info "启动 pPowerManger ..."
( cd "${WORKDIR}" && exec "${PM_BIN}" "${MISSION}" ) > "${WORKDIR}/pm.log" 2>&1 &
PM_PID=$!
sleep 1
kill -0 "${PM_PID}" 2>/dev/null || warn "pPowerManger 未存活(无 CCU 硬件属正常)"
}
start_host() {
info "启动 pPowerMangerHost (网页端口 ${WEB_PORT}) ..."
( cd "${WORKDIR}" && exec "${HOST_BIN}" "${MISSION}" ) > "${WORKDIR}/host.log" 2>&1 &
HOST_PID=$!
sleep 2
kill -0 "${HOST_PID}" 2>/dev/null || { err "pPowerMangerHost 未存活"; return 1; }
}
#-----------------------------------------------------------------------
# 测试项
#-----------------------------------------------------------------------
test_http() {
info "测试: 网页 HTTP (http://${SERVER_HOST}:${WEB_PORT}/) ..."
local code
code="$(curl -s -o /dev/null -w '%{http_code}' "http://${SERVER_HOST}:${WEB_PORT}/" 2>/dev/null || echo 000)"
if [ "${code}" = "200" ]; then
ok "网页 HTTP 200 通过"
return 0
else
err "网页 HTTP 失败 (code=${code})"
return 1
fi
}
# 反馈链路:向 MOOSDB 注入一条假的反馈变量 → 宿主订阅后应经 WS 推给浏览器
test_monitor_loop() {
info "测试: 反馈链路 (MOOSDB -> pPowerMangerHost -> WebSocket) ..."
if [ -z "${POKE_BIN}" ]; then
warn "未找到 uPokeDB,跳过反馈链路测试"
return 0
fi
if ! python3 -c "import websockets" >/dev/null 2>&1; then
warn "未安装 python3 websockets,跳过反馈链路测试"
return 0
fi
local fb_key="uPower_pmState_fb"
local fb_val='{"workCondition_uint8":9,"currentMod_uint8":1,"powerState_uint8":1,"faultLevel_uint8":0,"soc_uint16":88,"sustainableTime_int64":3600,"leakage_uint8":0,"info_str":"host-sim-test"}'
# 先让 WS 客户端连接等待快照,再注入反馈
FB_KEY="${fb_key}" python3 - "${WEB_PORT}" > "${WORKDIR}/ws_fb.log" 2>&1 <<'PY' &
import asyncio, json, os, sys
import websockets
PORT = int(sys.argv[1])
KEY = os.environ["FB_KEY"]
async def main():
uri = f"ws://localhost:{PORT}/ws"
async with websockets.connect(uri) as ws:
try:
while True:
msg = await asyncio.wait_for(ws.recv(), timeout=8)
snap = json.loads(msg)
if isinstance(snap, dict) and KEY in snap:
print(f"FEEDBACK_OK {KEY}")
return
except asyncio.TimeoutError:
pass
print("FEEDBACK_TIMEOUT")
asyncio.run(main())
PY
WS_PID=$!
sleep 1
"${POKE_BIN}" --host="${SERVER_HOST}" --port="${SERVER_PORT}" "${fb_key}=${fb_val}" >/dev/null 2>&1
wait "${WS_PID}" 2>/dev/null
if grep -q "FEEDBACK_OK" "${WORKDIR}/ws_fb.log" 2>/dev/null; then
ok "反馈链路通过(${fb_key} 已推送到浏览器)"
return 0
else
err "反馈链路失败(WS 未收到 ${fb_key})"
cat "${WORKDIR}/ws_fb.log" >&2
return 1
fi
}
# 指令下发:网页 WS 下发 -> 宿主 Notify -> MOOSDB -> (宿主订阅回环) -> 推回浏览器
# 利用宿主订阅了 uPower_pmState_fb,下发该变量后经 MOOSDB 回环,能收到即证明
# 指令确实进入了 MOOSDB。
test_command_notify() {
info "测试: 指令下发 (WebSocket -> pPowerMangerHost -> MOOSDB) ..."
if ! python3 -c "import websockets" >/dev/null 2>&1; then
warn "未安装 python3 websockets,跳过指令下发校验"
return 0
fi
python3 - "${WEB_PORT}" > "${WORKDIR}/ws_cmd.log" 2>&1 <<'PY'
import asyncio, json, sys
import websockets
PORT = int(sys.argv[1])
KEY = "uPower_pmState_fb"
TAG = "cmd-loopback-test"
async def main():
uri = f"ws://localhost:{PORT}/ws"
async with websockets.connect(uri) as ws:
await ws.send(json.dumps({"key": KEY, "value": {"info_str": TAG, "workCondition_uint8": 7}}))
try:
while True:
msg = await asyncio.wait_for(ws.recv(), timeout=8)
snap = json.loads(msg)
if isinstance(snap, dict) and KEY in snap:
obj = json.loads(snap[KEY])
if obj.get("info_str") == TAG:
print("CMD_OK")
return
except asyncio.TimeoutError:
pass
print("CMD_TIMEOUT")
asyncio.run(main())
PY
local r=$?
if [ ${r} -eq 0 ] && grep -q "CMD_OK" "${WORKDIR}/ws_cmd.log" 2>/dev/null; then
ok "指令下发到 MOOSDB 通过(uPower_pmState_fb 已回环)"
return 0
else
err "指令下发校验失败:未收到回环快照"
cat "${WORKDIR}/ws_cmd.log" >&2
return 1
fi
}
#-----------------------------------------------------------------------
# 命令入口
#-----------------------------------------------------------------------
cmd_run() {
check_binaries || return 1
start_moosdb || return 1
[ "${WITH_POWERMANGER}" = "1" ] && start_pm
start_host || return 1
info "全部启动完成。"
info "浏览器访问: http://${SERVER_HOST}:${WEB_PORT}/"
info "按 Ctrl-C 退出并清理。"
echo ""
trap 'info "退出清理..."; cleanup; exit 0' INT TERM
while kill -0 "${HOST_PID}" 2>/dev/null; do sleep 1; done
info "pPowerMangerHost 已退出。"
cleanup
}
cmd_test() {
check_binaries || return 1
start_moosdb || return 1
start_host || return 1
local pass=0 fail=0 r
test_http; r=$?; [ $r -eq 0 ] && pass=$((pass+1)) || fail=$((fail+1))
test_monitor_loop; r=$?; [ $r -eq 0 ] && pass=$((pass+1)) || fail=$((fail+1))
test_command_notify;r=$?; [ $r -eq 0 ] && pass=$((pass+1)) || fail=$((fail+1))
echo ""
info "==== 测试结果: 通过 ${pass} / 失败 ${fail} ===="
cleanup
[ "${fail}" -eq 0 ]
}
cmd_stop() {
pkill -f "${HOST_BIN}" 2>/dev/null
pkill -f "${PM_BIN}" 2>/dev/null
pkill -x MOOSDB 2>/dev/null
ok "已清理残留进程"
}
case "${1:-run}" in
run) cmd_run ;;
test) cmd_test ;;
build) build_targets ;;
stop) cmd_stop ;;
help|-h|--help) usage ;;
*) usage ;;
esac
+1
View File
@@ -19,6 +19,7 @@ INCLUDE_DIRECTORIES(${LOCAL_LIBRARY_DIRS})
ADD_SUBDIRECTORY(pPowerManger) ADD_SUBDIRECTORY(pPowerManger)
# ADD_SUBDIRECTORY(appPowerMangerWebUI) # ADD_SUBDIRECTORY(appPowerMangerWebUI)
add_subdirectory(pPMtest) add_subdirectory(pPMtest)
add_subdirectory(pPowerMangerHost)
############################################################################## ##############################################################################
# END of CMakeLists.txt # END of CMakeLists.txt
############################################################################## ##############################################################################
+1 -1
View File
@@ -865,7 +865,7 @@ void HttpServer::handleLogRequest(struct mg_connection* c, const std::string& ms
response["type"] = "log"; response["type"] = "log";
response["page"] = page; response["page"] = page;
response["pageSize"] = pageSize; response["pageSize"] = pageSize;
response["total"] = static_cast<unsigned long long>(logs.size()); // 返回总条数 response["total"] = static_cast<Json::UInt64>(logs.size()); // 返回总条数
response["content"] = Json::arrayValue; response["content"] = Json::arrayValue;
if (reverse) { if (reverse) {
+4 -4
View File
@@ -152,7 +152,7 @@ public:
m_state_of_bus_hv.set_fb_reserved_breaker((data.power_bus_status.fbReservedCircuitBreaker == 1)); m_state_of_bus_hv.set_fb_reserved_breaker((data.power_bus_status.fbReservedCircuitBreaker == 1));
m_state_of_bus_hv.set_tyz_reserved_breaker((data.power_bus_status.tyzReservedCircuitBreaker == 1)); m_state_of_bus_hv.set_tyz_reserved_breaker((data.power_bus_status.tyzReservedCircuitBreaker == 1));
m_state_of_bus_hv.set_reserved_breaker((data.power_bus_status.reservedCircuitBreaker == 1)); m_state_of_bus_hv.set_reserved_breaker((data.power_bus_status.reservedCircuitBreaker == 1));
dis["HVolABusBreaker_uint16"] = static_cast<unsigned long long>(m_state_of_bus_hv.relay_states.to_ulong()); dis["HVolABusBreaker_uint16"] = static_cast<Json::UInt64>(m_state_of_bus_hv.relay_states.to_ulong());
m_state_of_bus_hv_a.set_bow_ft_device_1_3_breaker((data.power_a_bus_status.bowFTDevice123CircuitBreaker == 1)); m_state_of_bus_hv_a.set_bow_ft_device_1_3_breaker((data.power_a_bus_status.bowFTDevice123CircuitBreaker == 1));
@@ -161,7 +161,7 @@ public:
m_state_of_bus_hv_a.set_xcz_breaker((data.power_a_bus_status.xczCircuitBreaker == 1)); m_state_of_bus_hv_a.set_xcz_breaker((data.power_a_bus_status.xczCircuitBreaker == 1));
m_state_of_bus_hv_a.set_bow_rudder_control_box_breaker((data.power_a_bus_status.bowRudderControlBoxCircuitBreaker == 1)); m_state_of_bus_hv_a.set_bow_rudder_control_box_breaker((data.power_a_bus_status.bowRudderControlBoxCircuitBreaker == 1));
m_state_of_bus_hv_a.set_open_water_cover_actuator_breaker((data.power_a_bus_status.openWaterCoverStartCylinderCircuitBreaker == 1)); m_state_of_bus_hv_a.set_open_water_cover_actuator_breaker((data.power_a_bus_status.openWaterCoverStartCylinderCircuitBreaker == 1));
dis["HVolBBusBreaker_uint16"] = static_cast<unsigned long long>(m_state_of_bus_hv_a.relay_states.to_ulong()); dis["HVolBBusBreaker_uint16"] = static_cast<Json::UInt64>(m_state_of_bus_hv_a.relay_states.to_ulong());
m_state_of_bus_lv.set_bow_low_voltage_box_breaker((data.instrument_main_bus_status.bowLowVoltageDistributionBoxCircuitBreaker == 1)); m_state_of_bus_lv.set_bow_low_voltage_box_breaker((data.instrument_main_bus_status.bowLowVoltageDistributionBoxCircuitBreaker == 1));
m_state_of_bus_lv.set_dc_c1_dc_distribution_board_breaker((data.instrument_main_bus_status.dcC1DCDistributionPanelCircuitBreaker == 1)); m_state_of_bus_lv.set_dc_c1_dc_distribution_board_breaker((data.instrument_main_bus_status.dcC1DCDistributionPanelCircuitBreaker == 1));
@@ -170,7 +170,7 @@ public:
m_state_of_bus_lv.set_emergency_lithium_battery_group_2_breaker((data.instrument_main_bus_status.emergencyLithiumBatteryGroup2CircuitBreaker == 1)); m_state_of_bus_lv.set_emergency_lithium_battery_group_2_breaker((data.instrument_main_bus_status.emergencyLithiumBatteryGroup2CircuitBreaker == 1));
m_state_of_bus_lv.set_reserved_breaker_1((data.instrument_main_bus_status.reservedCircuitBreaker1 == 1)); m_state_of_bus_lv.set_reserved_breaker_1((data.instrument_main_bus_status.reservedCircuitBreaker1 == 1));
m_state_of_bus_lv.set_reserved_breaker_2((data.instrument_main_bus_status.reservedCircuitBreaker2 == 1)); m_state_of_bus_lv.set_reserved_breaker_2((data.instrument_main_bus_status.reservedCircuitBreaker2 == 1));
dis["HVolBusBreaker_uint16"] = static_cast<unsigned long long>(m_state_of_bus_lv.relay_states.to_ulong()); dis["HVolBusBreaker_uint16"] = static_cast<Json::UInt64>(m_state_of_bus_lv.relay_states.to_ulong());
m_state_of_bus_lv_a.set_unit1_breaker((data.instrument_bus_status.unit1CircuitBreaker == 1)); m_state_of_bus_lv_a.set_unit1_breaker((data.instrument_bus_status.unit1CircuitBreaker == 1));
@@ -181,7 +181,7 @@ public:
m_state_of_bus_lv_a.set_reserved_breaker_1((data.instrument_bus_status.reservedCircuitBreaker1 == 1)); m_state_of_bus_lv_a.set_reserved_breaker_1((data.instrument_bus_status.reservedCircuitBreaker1 == 1));
m_state_of_bus_lv_a.set_reserved_breaker_2((data.instrument_bus_status.reservedCircuitBreaker2 == 1)); m_state_of_bus_lv_a.set_reserved_breaker_2((data.instrument_bus_status.reservedCircuitBreaker2 == 1));
m_state_of_bus_lv_a.set_emergency_lithium_battery_group_1_breaker((data.instrument_bus_status.emergencyLithiumBatteryGroup1CircuitBreaker == 1)); m_state_of_bus_lv_a.set_emergency_lithium_battery_group_1_breaker((data.instrument_bus_status.emergencyLithiumBatteryGroup1CircuitBreaker == 1));
dis["LVolBusBreaker_uint16"] = static_cast<unsigned long long>(m_state_of_bus_lv_a.relay_states.to_ulong()); dis["LVolBusBreaker_uint16"] = static_cast<Json::UInt64>(m_state_of_bus_lv_a.relay_states.to_ulong());
//构建配电系统故障码 //构建配电系统故障码
Json::Value faultCodeDis_array = Json::Value(Json::arrayValue); Json::Value faultCodeDis_array = Json::Value(Json::arrayValue);
+115
View File
@@ -0,0 +1,115 @@
#--------------------------------------------------------
# The CMakeLists.txt for: pPowerMangerHost
# Description: 模拟 pPowerManger 上位机(发指令 / 监控),
# 通过 mongoose HTTP+WebSocket 提供网页交互。
# 复用 src/pPowerManger/httpserver/mongoose.c,
# 不新增任何第三方依赖。
#--------------------------------------------------------
SET(MONGOOSE_SRC
${CMAKE_SOURCE_DIR}/src/pPowerManger/httpserver/mongoose.c)
SET(SRC
HostSim.cpp
HostSim_Info.cpp
WebServer.cpp
main.cpp
${MONGOOSE_SRC}
)
#--------------------------------------------------------
# web/ 目录 -> webassets_gen.h : 通用嵌入式资源表
# 扫描 web/ 下所有文件,逐个读成字节数组,并生成 {路径, MIME, 数据} 注册表。
# 新增页面/资源只需放入 web/ 目录,CMake 自动重新生成(含重新 configure)。
#--------------------------------------------------------
SET(WEB_ASSET_DIR ${CMAKE_CURRENT_SOURCE_DIR}/web)
SET(WEB_ASSET_HDR ${CMAKE_CURRENT_BINARY_DIR}/webassets_gen.h)
FILE(GLOB WEB_ASSET_FILES ${WEB_ASSET_DIR}/*)
LIST(SORT WEB_ASSET_FILES)
LIST(LENGTH WEB_ASSET_FILES WEB_ASSET_COUNT)
IF(NOT WEB_ASSET_FILES)
MESSAGE(FATAL_ERROR "web/ 目录为空,请至少放入一个页面资源")
ENDIF()
# 目录内文件增删/修改时自动重新 configure
SET_PROPERTY(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS
${WEB_ASSET_DIR} ${WEB_ASSET_FILES})
SET(WEB_ASSET_HEADER
"// Auto-generated from web/, DO NOT EDIT
#ifndef WEBASSETS_GEN_H
#define WEBASSETS_GEN_H
#include <string.h>
namespace webassets {
struct Asset {
const char* path;
const char* mime;
const unsigned char* data;
unsigned long len;
};
")
SET(WEB_ASSET_ARRAYS "")
SET(WEB_ASSET_REGISTRY "")
FOREACH(f ${WEB_ASSET_FILES})
GET_FILENAME_COMPONENT(name ${f} NAME)
STRING(MAKE_C_IDENTIFIER ${name} arrname)
# 读成十六进制字节序列,再转成 C 数组初始化
FILE(READ ${f} hexdata HEX)
STRING(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," hexdata "${hexdata}")
# 按扩展名推断 MIME(if/elseif 体内命令须另起一行)
SET(mime "application/octet-stream")
IF(name MATCHES "\\.(html|htm)$")
SET(mime "text/html; charset=utf-8")
ELSEIF(name MATCHES "\\.css$")
SET(mime "text/css")
ELSEIF(name MATCHES "\\.js$")
SET(mime "application/javascript")
ELSEIF(name MATCHES "\\.json$")
SET(mime "application/json")
ELSEIF(name MATCHES "\\.png$")
SET(mime "image/png")
ELSEIF(name MATCHES "\\.(jpe?g)$")
SET(mime "image/jpeg")
ELSEIF(name MATCHES "\\.gif$")
SET(mime "image/gif")
ELSEIF(name MATCHES "\\.svg$")
SET(mime "image/svg+xml")
ELSEIF(name MATCHES "\\.(woff2?)$")
SET(mime "font/woff2")
ELSEIF(name MATCHES "\\.txt$")
SET(mime "text/plain")
ENDIF()
STRING(APPEND WEB_ASSET_ARRAYS
"static const unsigned char ${arrname}[] = { ${hexdata} };\n\n")
STRING(APPEND WEB_ASSET_REGISTRY
" { \"${name}\", \"${mime}\", ${arrname}, sizeof(${arrname}) },\n")
ENDFOREACH()
FILE(WRITE ${WEB_ASSET_HDR}
"${WEB_ASSET_HEADER}${WEB_ASSET_ARRAYS}static const Asset REGISTRY[] = {\n${WEB_ASSET_REGISTRY}};\n\ninline const Asset* find(const char* path) {\n const unsigned long n = sizeof(REGISTRY) / sizeof(REGISTRY[0]);\n for (unsigned long i = 0; i < n; ++i) {\n if (strcmp(REGISTRY[i].path, path) == 0) return &REGISTRY[i];\n }\n return NULL;\n}\n\n}\n#endif\n")
MESSAGE(STATUS "Generated ${WEB_ASSET_HDR} (${WEB_ASSET_COUNT} files) from ${WEB_ASSET_DIR}")
ADD_EXECUTABLE(pPowerMangerHost ${SRC})
TARGET_INCLUDE_DIRECTORIES(pPowerMangerHost PRIVATE
${CMAKE_SOURCE_DIR}/src/pPowerManger/httpserver
${CMAKE_CURRENT_BINARY_DIR})
TARGET_LINK_LIBRARIES(pPowerMangerHost
${MOOS_LIBRARIES}
mbutil
m
pthread
jsoncpp)
+229
View File
@@ -0,0 +1,229 @@
/************************************************************/
/* NAME: */
/* ORGN: MIT */
/* FILE: HostSim.cpp */
/* DATE: */
/************************************************************/
#include <iterator>
#include <sstream>
#include "MBUtils.h"
#include "HostSim.h"
#include "WebServer.h"
#include "json/json.h"
using namespace std;
//---------------------------------------------------------
// Constructor
HostSim::HostSim()
: m_feedbackDirty(false), m_webServer(nullptr), m_webPort(18080)
{
}
//---------------------------------------------------------
// Destructor
HostSim::~HostSim()
{
if (m_webServer) {
m_webServer->stop();
delete m_webServer;
m_webServer = nullptr;
}
}
//---------------------------------------------------------
// Procedure: OnNewMail
bool HostSim::OnNewMail(MOOSMSG_LIST &NewMail)
{
MOOSMSG_LIST::iterator p;
for (p = NewMail.begin(); p != NewMail.end(); p++) {
CMOOSMsg &msg = *p;
string key = msg.GetKey();
if (msg.IsString()) {
lock_guard<mutex> lock(m_feedbackMutex);
m_feedback[key] = msg.GetString();
m_feedbackDirty = true;
}
}
return true;
}
//---------------------------------------------------------
// Procedure: OnConnectToServer
bool HostSim::OnConnectToServer()
{
RegisterVariables();
return true;
}
//---------------------------------------------------------
// Procedure: Iterate()
// happens AppTick times per second
bool HostSim::Iterate()
{
// 1. 处理网页下发的指令 -> Notify 到 MOOSDB
drainCommands();
// 2. 若有反馈更新,推送快照到所有网页客户端
bool dirty = false;
{
lock_guard<mutex> lock(m_feedbackMutex);
dirty = m_feedbackDirty;
}
if (dirty && m_webServer) {
m_webServer->broadcast(buildSnapshot());
lock_guard<mutex> lock(m_feedbackMutex);
m_feedbackDirty = false;
}
return true;
}
//---------------------------------------------------------
// Procedure: OnStartUp()
bool HostSim::OnStartUp()
{
list<string> sParams;
m_MissionReader.EnableVerbatimQuoting(false);
if (m_MissionReader.GetConfiguration(GetAppName(), sParams)) {
list<string>::iterator p;
for (p = sParams.begin(); p != sParams.end(); p++) {
string line = *p;
string param = tolower(biteStringX(line, '='));
string value = line;
if (param == "web_port") {
m_webPort = atoi(value.c_str());
}
}
}
// 启动网页服务器
m_webServer = new WebServer();
m_webServer->setOnMessage([this](const string& msg) {
this->handleWebMessage(msg);
});
m_webServer->start(m_webPort);
RegisterVariables();
return true;
}
//---------------------------------------------------------
// Procedure: handleWebMessage
// 网页下发 JSON: {"key":"...", "value":{...}} 或
// {"key":"...", "value":"..."},把 value 序列化为字符串后入队
void HostSim::handleWebMessage(const std::string& msg)
{
Json::CharReaderBuilder builder;
Json::Value root;
JSONCPP_STRING errs;
istringstream stream(msg);
if (!Json::parseFromStream(builder, stream, &root, &errs)) {
return;
}
if (!root.isMember("key")) {
return;
}
string key = root["key"].asString();
string valueStr;
if (root.isMember("value")) {
const Json::Value& v = root["value"];
if (v.isString()) {
// 允许直接发送原始 JSON 字符串
valueStr = v.asString();
} else {
Json::StreamWriterBuilder wb;
wb.settings_["indentation"] = "";
valueStr = Json::writeString(wb, v);
}
}
pushCommand(key, valueStr);
}
//---------------------------------------------------------
// Procedure: pushCommand
void HostSim::pushCommand(const string& key, const string& jsonValue)
{
lock_guard<mutex> lock(m_cmdMutex);
m_cmdQueue.push(make_pair(key, jsonValue));
}
//---------------------------------------------------------
// Procedure: drainCommands
bool HostSim::drainCommands()
{
vector<pair<string, string> > pending;
{
lock_guard<mutex> lock(m_cmdMutex);
while (!m_cmdQueue.empty()) {
pending.push_back(m_cmdQueue.front());
m_cmdQueue.pop();
}
}
for (size_t i = 0; i < pending.size(); i++) {
Notify(pending[i].first, pending[i].second);
}
return true;
}
//---------------------------------------------------------
// Procedure: buildSnapshot
string HostSim::buildSnapshot()
{
Json::Value root(Json::objectValue);
{
lock_guard<mutex> lock(m_feedbackMutex);
for (map<string, string>::iterator it = m_feedback.begin();
it != m_feedback.end(); ++it) {
root[it->first] = it->second;
}
}
Json::StreamWriterBuilder wb;
wb.settings_["indentation"] = "";
return Json::writeString(wb, root);
}
//---------------------------------------------------------
// Procedure: RegisterVariables
void HostSim::RegisterVariables()
{
registerFeedbackVars();
}
//---------------------------------------------------------
// Procedure: registerFeedbackVars
// 订阅 pPowerManger 上位机反馈变量
void HostSim::registerFeedbackVars()
{
// 状态报告
Register("uPower_pmState_fb", 0); // 能源管理总状态
Register("uPower_fcsState_st", 0); // 燃料电池状态
Register("uPower_liBatState_st", 0); // 锂电池状态
Register("uPower_disSysState_st", 0); // 配电系统状态
Register("uPower_currentState_st", 0); // 母线电压电流
Register("uPower_equState_fb", 0); // 设备状态
Register("uPower_equInsState_fb", 0); // 设备仪表状态
Register("uPower_secdistState_cmd", 0);// 次级配电状态
Register("uPower_subDisFault_st", 0); // 次级配电故障状态(_DEBUG 下发布)
}
+61
View File
@@ -0,0 +1,61 @@
/************************************************************/
/* NAME: */
/* ORGN: MIT */
/* FILE: HostSim.h */
/* DATE: */
/************************************************************/
#ifndef HOSTSIM_HEADER
#define HOSTSIM_HEADER
#include "MOOS/libMOOS/MOOSLib.h"
#include <string>
#include <map>
#include <vector>
#include <mutex>
#include <queue>
#include <atomic>
class WebServer;
// 模拟 pPowerManger 上位机:网页 <-> MOOSDB 桥
class HostSim : public CMOOSApp
{
public:
HostSim();
~HostSim();
protected: // Standard MOOSApp functions to overload
bool OnNewMail(MOOSMSG_LIST &NewMail);
bool Iterate();
bool OnConnectToServer();
bool OnStartUp();
void RegisterVariables();
public: // 由 WebServer 线程调用
// 网页下发的指令:key 为 MOOS 变量名,jsonValue 为已序列化的 JSON 字符串
void pushCommand(const std::string& key, const std::string& jsonValue);
// 取当前所有反馈变量的 JSON 快照
std::string buildSnapshot();
private:
void registerFeedbackVars();
bool drainCommands();
void handleWebMessage(const std::string& msg);
private: // State variables
// 反馈缓存:key -> 最新 JSON 字符串
std::mutex m_feedbackMutex;
std::map<std::string, std::string> m_feedback;
bool m_feedbackDirty;
// 待发送指令队列
std::mutex m_cmdMutex;
std::queue<std::pair<std::string, std::string> > m_cmdQueue;
// 网页服务器
WebServer* m_webServer;
int m_webPort;
};
#endif
+114
View File
@@ -0,0 +1,114 @@
/****************************************************************/
/* NAME: */
/* ORGN: MIT Cambridge MA */
/* FILE: HostSim_Info.cpp */
/* DATE: */
/****************************************************************/
#include <cstdlib>
#include <iostream>
#include "HostSim_Info.h"
#include "ColorParse.h"
#include "ReleaseInfo.h"
using namespace std;
//----------------------------------------------------------------
// Procedure: showSynopsis
void showSynopsis()
{
blk("SYNOPSIS: ");
blk("------------------------------------ ");
blk(" pPowerMangerHost 模拟 pPowerManger 的上位机。 ");
blk(" 通过 MOOSDB 与 pPowerManger 交互,并使用网页进行界面交互: ");
blk(" 下发指令 / 监控反馈,浏览器访问 http://<host>:18080 。 ");
blk(" ");
}
//----------------------------------------------------------------
// Procedure: showHelpAndExit
void showHelpAndExit()
{
blk(" ");
blu("=============================================================== ");
blu("Usage: pPowerMangerHost file.moos [OPTIONS] ");
blu("=============================================================== ");
blk(" ");
showSynopsis();
blk(" ");
blk("Options: ");
mag(" --alias","=<ProcessName> ");
blk(" Launch pPowerMangerHost with the given process name. ");
mag(" --example, -e ");
blk(" Display example MOOS configuration block. ");
mag(" --help, -h ");
blk(" Display this help message. ");
mag(" --interface, -i ");
blk(" Display MOOS publications and subscriptions. ");
mag(" --version,-v ");
blk(" Display the release version of pPowerMangerHost. ");
blk(" ");
exit(0);
}
//----------------------------------------------------------------
// Procedure: showExampleConfigAndExit
void showExampleConfigAndExit()
{
blk(" ");
blu("=============================================================== ");
blu("pPowerMangerHost Example MOOS Configuration ");
blu("=============================================================== ");
blk(" ");
blk("ProcessConfig = pPowerMangerHost ");
blk("{ ");
blk(" AppTick = 4 ");
blk(" CommsTick = 4 ");
blk(" WEB_PORT = 18080 // 网页监听端口 ");
blk("} ");
blk(" ");
exit(0);
}
//----------------------------------------------------------------
// Procedure: showInterfaceAndExit
void showInterfaceAndExit()
{
blk(" ");
blu("=============================================================== ");
blu("pPowerMangerHost INTERFACE ");
blu("=============================================================== ");
blk(" ");
showSynopsis();
blk(" ");
blk("SUBSCRIPTIONS: ");
blk("------------------------------------ ");
blk(" uPower_pmState_fb 能源管理总状态 ");
blk(" uPower_fcsState_st 燃料电池状态 ");
blk(" uPower_liBatState_st 锂电池状态 ");
blk(" uPower_disSysState_st 配电系统状态 ");
blk(" uPower_currentState_st 母线电压电流 ");
blk(" uPower_equState_fb 设备状态 ");
blk(" uPower_equInsState_fb 设备仪表状态 ");
blk(" uPower_secdistState_cmd 次级配电状态 ");
blk(" ");
blk("PUBLICATIONS: ");
blk("------------------------------------ ");
blk(" 由网页下发,任意 MOOS 变量(命令类)Notify 到 MOOSDB。 ");
blk(" 例如 uExternComm_setOpCondition_cmd / uPlan_taskStart_st 等。 ");
blk(" ");
exit(0);
}
//----------------------------------------------------------------
// Procedure: showReleaseInfoAndExit
void showReleaseInfoAndExit()
{
showReleaseInfo("pPowerMangerHost", "gpl");
exit(0);
}
+17
View File
@@ -0,0 +1,17 @@
/****************************************************************/
/* NAME: */
/* ORGN: MIT Cambridge MA */
/* FILE: HostSim_Info.h */
/* DATE: */
/****************************************************************/
#ifndef HOSTSIM_INFO_HEADER
#define HOSTSIM_INFO_HEADER
void showSynopsis();
void showHelpAndExit();
void showExampleConfigAndExit();
void showInterfaceAndExit();
void showReleaseInfoAndExit();
#endif
+115
View File
@@ -0,0 +1,115 @@
#include "WebServer.h"
#include "webassets_gen.h"
#include <iostream>
//---------------------------------------------------------
// 事件处理:HTTP 路由 + WebSocket 收发
void WebServer::handleEvent(struct mg_connection* c, int ev, void* ev_data) {
if (ev == MG_EV_HTTP_MSG) {
struct mg_http_message* hm = (struct mg_http_message*)ev_data;
// WebSocket 升级
if (mg_match(hm->uri, mg_str("/ws"), NULL)) {
mg_ws_upgrade(c, hm, NULL);
return;
}
// 静态资源路由:在嵌入式资源表 webassets 中按路径查找并返回
std::string path(hm->uri.buf, hm->uri.len);
if (path == "/" || path == "/index.html") {
path = "index.html";
} else if (!path.empty() && path[0] == '/') {
path = path.substr(1);
}
const webassets::Asset* a = webassets::find(path.c_str());
if (a) {
std::string hdr = "Content-Type: ";
hdr += a->mime;
hdr += "\r\n";
mg_http_reply(c, 200, hdr.c_str(), "%.*s", (int) a->len,
(const char*) a->data);
} else {
mg_http_reply(c, 404, "Content-Type: text/plain\r\n", "Not Found\n");
}
} else if (ev == MG_EV_WS_OPEN) {
std::lock_guard<std::mutex> lock(m_wsMutex);
m_wsConnections.push_back(c);
} else if (ev == MG_EV_CLOSE || ev == MG_EV_ERROR) {
std::lock_guard<std::mutex> lock(m_wsMutex);
for (auto it = m_wsConnections.begin(); it != m_wsConnections.end(); ++it) {
if (*it == c) {
m_wsConnections.erase(it);
break;
}
}
} else if (ev == MG_EV_WS_MSG) {
struct mg_ws_message* wm = (struct mg_ws_message*)ev_data;
std::string msg(wm->data.buf, wm->data.len);
if (m_onMessage) {
m_onMessage(msg);
}
}
}
WebServer::WebServer() : m_port(0), m_running(false) {
mg_mgr_init(&m_mgr);
}
WebServer::~WebServer() {
stop();
mg_mgr_free(&m_mgr);
}
void WebServer::setOnMessage(std::function<void(const std::string&)> handler) {
m_onMessage = handler;
}
bool WebServer::start(int port) {
if (m_running) return true;
m_port = port;
std::string addr = "http://0.0.0.0:" + std::to_string(port);
mg_http_listen(&m_mgr, addr.c_str(), [](mg_connection* c, int ev, void* ev_data) {
WebServer* server = static_cast<WebServer*>(c->fn_data);
if (server) server->handleEvent(c, ev, ev_data);
}, this);
m_running = true;
m_thread = std::thread([this]() { serverThreadFunc(); });
std::cout << "pPowerMangerHost web UI listening on " << addr << std::endl;
return true;
}
void WebServer::stop() {
if (!m_running) return;
m_running = false;
if (m_thread.joinable()) m_thread.join();
std::lock_guard<std::mutex> lock(m_wsMutex);
for (auto c : m_wsConnections) {
mg_ws_send(c, "", 0, WEBSOCKET_OP_CLOSE);
}
m_wsConnections.clear();
}
bool WebServer::serverThreadCB(void* pParam) {
WebServer* pThis = static_cast<WebServer*>(pParam);
return pThis->serverThreadFunc();
}
bool WebServer::serverThreadFunc() {
while (m_running) {
mg_mgr_poll(&m_mgr, 200);
}
return true;
}
void WebServer::broadcast(const std::string& text) {
std::lock_guard<std::mutex> lock(m_wsMutex);
for (auto c : m_wsConnections) {
mg_ws_send(c, text.c_str(), text.size(), WEBSOCKET_OP_TEXT);
}
}
+42
View File
@@ -0,0 +1,42 @@
#ifndef WEBSERVER_H
#define WEBSERVER_H
#include "mongoose.h"
#include <string>
#include <functional>
#include <vector>
#include <mutex>
#include <atomic>
#include <thread>
class WebServer {
public:
WebServer();
~WebServer();
// 启动 HTTP+WebSocket 服务器(阻塞在独立线程)
bool start(int port);
void stop();
// 向所有 WebSocket 客户端广播文本
void broadcast(const std::string& text);
// 收到网页 WS 消息时的回调
void setOnMessage(std::function<void(const std::string&)> handler);
private:
static bool serverThreadCB(void* pParam);
bool serverThreadFunc();
void handleEvent(struct mg_connection* c, int ev, void* ev_data);
struct mg_mgr m_mgr;
int m_port;
std::atomic<bool> m_running;
std::thread m_thread;
std::mutex m_wsMutex;
std::vector<mg_connection*> m_wsConnections;
std::function<void(const std::string&)> m_onMessage;
};
#endif // WEBSERVER_H
+51
View File
@@ -0,0 +1,51 @@
/************************************************************/
/* NAME: */
/* ORGN: MIT */
/* FILE: main.cpp */
/* DATE: */
/************************************************************/
#include <string>
#include "MBUtils.h"
#include "ColorParse.h"
#include "HostSim.h"
#include "HostSim_Info.h"
using namespace std;
int main(int argc, char *argv[])
{
string mission_file;
string run_command = argv[0];
for(int i=1; i<argc; i++) {
string argi = argv[i];
if((argi=="-v") || (argi=="--version") || (argi=="-version"))
showReleaseInfoAndExit();
else if((argi=="-e") || (argi=="--example") || (argi=="-example"))
showExampleConfigAndExit();
else if((argi == "-h") || (argi == "--help") || (argi=="-help"))
showHelpAndExit();
else if((argi == "-i") || (argi == "--interface"))
showInterfaceAndExit();
else if(strEnds(argi, ".moos") || strEnds(argi, ".moos++"))
mission_file = argv[i];
else if(strBegins(argi, "--alias="))
run_command = argi.substr(8);
else if(i==2)
run_command = argi;
}
if(mission_file == "")
showHelpAndExit();
cout << termColor("green");
cout << "pPowerMangerHost launching as " << run_command << endl;
cout << termColor() << endl;
HostSim hostSim;
hostSim.Run(run_command.c_str(), mission_file.c_str());
return(0);
}
@@ -0,0 +1,8 @@
//SERVERHOST = 192.168.0.223
//SERVERPORT = 9000
ProcessConfig = pPowerMangerHost
{
AppTick = 4
CommsTick = 4
WEB_PORT = 18080
}
+720
View File
@@ -0,0 +1,720 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>pPowerManger 上位机模拟</title>
<style>
:root {
--bg: #0f172a;
--panel: #1e293b;
--border: #334155;
--fg: #e2e8f0;
--acc: #38bdf8;
--ok: #22c55e;
--warn: #f59e0b;
--err: #ef4444;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Microsoft YaHei", sans-serif;
background: var(--bg);
color: var(--fg);
}
header {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 20px;
background: var(--panel);
border-bottom: 1px solid var(--border);
}
header h1 { font-size: 16px; margin: 0; }
.dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--err);
}
.dot.on { background: var(--ok); }
main {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
padding: 16px;
max-width: 1500px;
margin: 0 auto;
}
@media (max-width: 900px) {
main { grid-template-columns: 1fr; }
}
section {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 10px;
padding: 14px;
}
section.wide {
margin: 0 16px 16px;
max-width: 1468px;
}
section h2 {
font-size: 14px;
margin: 0 0 10px;
color: var(--acc);
}
.row {
display: flex;
gap: 8px;
flex-wrap: wrap;
align-items: center;
margin-bottom: 8px;
}
label {
font-size: 12px;
color: #94a3b8;
}
select, input, textarea, button {
background: #0b1220;
color: var(--fg);
border: 1px solid var(--border);
border-radius: 6px;
padding: 6px 8px;
font-size: 13px;
}
textarea {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
width: 100%;
min-height: 90px;
resize: vertical;
}
button {
cursor: pointer;
background: #0ea5e9;
border-color: #0ea5e9;
color: #fff;
}
button:hover { filter: brightness(1.1); }
button.sec {
background: #334155;
border-color: #475569;
}
.chip {
display: inline-block;
padding: 3px 8px;
margin: 2px;
border-radius: 999px;
border: 1px solid var(--border);
cursor: pointer;
font-size: 12px;
}
.chip:hover { border-color: var(--acc); }
.var-item {
border: 1px solid var(--border);
border-radius: 8px;
margin-bottom: 8px;
overflow: hidden;
}
.var-item summary {
cursor: pointer;
padding: 8px 10px;
font-size: 13px;
background: #16202f;
user-select: none;
}
.var-item pre {
margin: 0;
padding: 10px;
font-size: 12px;
overflow-x: auto;
background: #0b1220;
color: #a5f3fc;
white-space: pre-wrap;
word-break: break-all;
}
.muted {
color: #64748b;
font-size: 12px;
}
#log {
font-size: 12px;
color: #94a3b8;
max-height: 180px;
overflow-y: auto;
}
#log .send { color: var(--acc); }
#log .err { color: var(--err); }
#chart {
width: 100%;
height: 280px;
display: block;
background: #0b1220;
border: 1px solid var(--border);
border-radius: 8px;
}
.serie {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 3px 8px;
margin: 2px;
border-radius: 999px;
border: 1px solid var(--border);
font-size: 12px;
background: #0b1220;
}
.serie .swatch {
width: 10px;
height: 10px;
border-radius: 2px;
display: inline-block;
}
.serie .rm {
cursor: pointer;
color: #94a3b8;
font-weight: bold;
}
.serie .rm:hover { color: var(--err); }
</style>
</head>
<body>
<header>
<span class="dot" id="wsDot"></span>
<h1>pPowerManger 上位机模拟 (Web UI)</h1>
<span class="muted" id="wsState">连接中…</span>
</header>
<main>
<section>
<h2>指令下发</h2>
<div class="row">
<label>命令类型</label>
<select id="cmdType"></select>
</div>
<div class="row">
<label>MOOS 变量</label>
<input id="cmdKey" style="flex:1" />
</div>
<div class="row">
<label>JSON 载荷</label>
</div>
<textarea id="cmdValue"></textarea>
<div class="row" style="margin-top:8px">
<button id="btnSend">发送</button>
<button id="btnReset" class="sec">重置模板</button>
</div>
<h2>快捷指令</h2>
<div id="quick"></div>
<h2>通用 MOOS 通知</h2>
<div class="row">
<input id="rawKey" placeholder="变量名" style="flex:1" />
<input id="rawValue" placeholder="JSON 字符串" style="flex:2" />
<button id="btnRawSend">Notify</button>
</div>
</section>
<section>
<h2>反馈监控(pPowerManger 发往上位机的全部消息)</h2>
<div class="muted" id="monitorMeta"></div>
<div id="monitor"></div>
</section>
</main>
<section class="wide">
<h2>连续数据曲线</h2>
<div class="row">
<label>选择字段</label>
<select id="fieldSel" style="flex:1"></select>
<button id="btnAddSeries">添加曲线</button>
<button id="btnClearSeries" class="sec">清空全部</button>
</div>
<div id="seriesBox" style="margin-bottom:8px"></div>
<canvas id="chart"></canvas>
</section>
<section class="wide">
<h2>操作日志</h2>
<div id="log"></div>
</section>
<script>
// ---- 数据定义 ----
const WORK_CONDITIONS = [
[0, "切换中"],
[1, "待机"],
[2, "岸基备航"],
[3, "水中备航"],
[4, "遥控"],
[5, "巡航"],
[6, "高速"],
[7, "上浮下潜"],
[8, "浮调"],
[9, "水下侦查"],
[10, "水面侦查"],
[11, "DJ"],
[12, "调试"]
];
const MODES = [
[0, "正常"],
[1, "测试"],
[2, "调试"]
];
const POWER_CMDS = [
[0, "断电"],
[1, "上电"],
[2, "保持"]
];
const TASKS = [
["throwLoad", "抛载"],
["floatUp", "运动上浮"],
["recycle", "回收"],
["hover", "悬停"],
["satComm", "上浮通信"],
["satCalibrate", "上浮校准"],
["tour", "游曳"],
["sail", "航行"],
["emerSatComm", "紧急上浮通信"],
["emerSail", "紧急航行"]
];
const DEVICES = [
[0, "singleBeamSonar1"],
[1, "singleBeamSonar2"],
[2, "singleBeamSonar3"],
[3, "singleBeamSonar4"],
[4, "singleBeamSonar5"],
[5, "singleBeamSonar6"],
[6, "singleBeamSonar7"],
[7, "singleBeamSonar8"],
[8, "forwardSonar"],
[9, "bowCover1"],
[10, "bowCover2"],
[11, "bowCover3"],
[12, "bowCover4"],
[13, "bowCover5"],
[14, "bowCover6"],
[15, "bowRudderDeploy"],
[16, "bowRudderServo1"],
[17, "bowRudderServo2"],
[18, "depth1"],
[19, "depth2"],
[20, "depth3"],
[21, "overDeep1"],
[22, "overDeep2"],
[23, "ctd1"],
[24, "ctd2"],
[25, "soundVeloc"],
[26, "waterInject"],
[27, "bowWaterInet1"],
[28, "bowWaterInet2"],
[29, "bowWaterInet3"],
[30, "bowWaterInet4"],
[31, "bowWaterInet5"],
[32, "bowWaterInet6"],
[33, "bowWaterInet7"],
[34, "bowWaterInet8"],
[35, "sideSonar"],
[36, "lowFreqSonar"],
[37, "launchControler"],
[38, "photoSensor"],
[39, "electSensor"],
[40, "commMast"],
[41, "mastLiftingServo"],
[42, "USBL"],
[43, "DVL"],
[44, "buoyancyAdjust1"],
[45, "buoyancyAdjust2"],
[46, "buoyancyAdjust3"],
[47, "buoyancyAdjust4"],
[48, "buoyancyAdjust5"],
[49, "sternRudderServo1"],
[50, "sternRudderServo2"],
[51, "sternRudderServo3"],
[52, "sternRudderServo4"],
[53, "thruster"],
[54, "bowThrow"],
[55, "sternThrow"],
[56, "buoyage"],
[57, "ins"],
[58, "ntp"],
[59, "inputWaterSensor1"],
[60, "inputWaterSensor2"],
[61, "inputWaterSensor3"]
];
const DEV_CMDS = [
[0, "断电(OFF)"],
[1, "上电(ON)"],
[2, "仪表上电"],
[3, "动力上电"],
[4, "仪表断电"],
[5, "动力断电"]
];
const COMMANDS = {
setOpCondition: {
key: "uExternComm_setOpCondition_cmd",
tpl: { state_uint8: 1 },
label: "设置工况"
},
setOpMode: {
key: "uExternComm_setOpMode_cmd",
tpl: { state_uint8: 0 },
label: "设置模式"
},
setPowerSystem: {
key: "uExternComm_setPowerSystem_cmd",
tpl: { powerCmd_uint8: 1 },
label: "电源系统"
},
masterControl: {
key: "uPower_sysState_cmd",
tpl: { powerCMD: 0, modCMD: 0, workCMD: 0 },
label: "主控指令"
},
setDeviceSwitch: {
key: "uExternComm_setDeviceSwitch_cmd",
tpl: { id_uint8: 0, cmd_uint8: 1 },
label: "设备开关"
},
taskStart: {
key: "uPlan_taskStart_st",
tpl: { taskType_str: "sail", balanceEnabled_bool: true, bowRudderUnfold_bool: true },
label: "任务启动"
},
planStop: {
key: "uPlan_planStop_st",
tpl: { op_int32: 1 },
label: "任务停止"
},
depth: {
key: "uDevice_depth_st",
tpl: { value_array_int32: [0, 0, 0] },
label: "深度"
},
equState: {
key: "uPower_equState_cmd",
tpl: {},
label: "设备指令(DriverTable)"
},
secdistState: {
key: "uDevice_secdistState_st",
tpl: {},
label: "次级配电状态"
}
};
// ---- 曲线相关 ----
const COLORS = [
"#38bdf8", "#22c55e", "#f59e0b", "#ef4444", "#a78bfa",
"#f472b6", "#34d399", "#fbbf24", "#fb923c", "#e879f9"
];
const MAX_POINTS = 300;
let history = {}; // path -> { color, points: [[t, v], ...] }
let allFields = new Set();
let nextColor = 0;
let ws = null;
let currentCmd = "setOpCondition";
// ---- 工具函数 ----
function el(id) {
return document.getElementById(id);
}
function log(msg, cls) {
const d = document.createElement("div");
if (cls) d.className = cls;
d.textContent = msg;
el("log").appendChild(d);
el("log").scrollTop = el("log").scrollHeight;
}
// ---- 通信 ----
function connect() {
const proto = location.protocol === "https:" ? "wss://" : "ws://";
ws = new WebSocket(proto + location.host + "/ws");
ws.onopen = () => {
el("wsDot").classList.add("on");
el("wsState").textContent = "已连接";
};
ws.onclose = () => {
el("wsDot").classList.remove("on");
el("wsState").textContent = "已断开,重连中…";
setTimeout(connect, 1000);
};
ws.onerror = () => ws.close();
ws.onmessage = (e) => {
try {
const snap = JSON.parse(e.data);
onSnapshot(snap);
} catch (err) {
log("解析反馈失败: " + err, "err");
}
};
}
function sendMsg(obj) {
if (!ws || ws.readyState !== WebSocket.OPEN) {
log("未连接", "err");
return;
}
ws.send(JSON.stringify(obj));
log("发送 -> " + obj.key + " : " + JSON.stringify(obj.value), "send");
}
// ---- 指令下发 ----
function applyTemplate(cmdType) {
const c = COMMANDS[cmdType];
el("cmdKey").value = c.key;
el("cmdValue").value = JSON.stringify(c.tpl, null, 2);
}
function sendCurrent() {
let v;
try {
v = JSON.parse(el("cmdValue").value);
} catch (e) {
log("JSON 非法: " + e.message, "err");
return;
}
sendMsg({ key: el("cmdKey").value, value: v });
}
function buildQuick(){
const box = el("quick");
let html = "";
html += '<div class="muted">工况 (setOpCondition)</div><div>';
WORK_CONDITIONS.forEach(([v,n]) => { html += `<span class="chip" data-act="cond" data-v="${v}">${n}</span>`; });
html += "</div>";
html += '<div class="muted" style="margin-top:8px">模式 (setOpMode)</div><div>';
MODES.forEach(([v,n]) => { html += `<span class="chip" data-act="mode" data-v="${v}">${n}</span>`; });
html += "</div>";
html += '<div class="muted" style="margin-top:8px">电源 (setPowerSystem)</div><div>';
POWER_CMDS.forEach(([v,n]) => { html += `<span class="chip" data-act="power" data-v="${v}">${n}</span>`; });
html += "</div>";
html += '<div class="muted" style="margin-top:8px">任务启动 (taskStart)</div><div>';
TASKS.forEach(([v,n]) => { html += `<span class="chip" data-act="task" data-v="${v}">${n}</span>`; });
html += "</div>";
html += '<div class="muted" style="margin-top:8px">设备开关 (setDeviceSwitch)</div>';
html += '<div class="row"><select id="devSel">' + DEVICES.map(([v,n])=>`<option value="${v}">${n}</option>`).join("") + "</select>" +
'<select id="devCmd">' + DEV_CMDS.map(([v,n])=>`<option value="${v}">${n}</option>`).join("") + "</select>" +
'<button id="btnDev">执行</button></div>';
box.innerHTML = html;
box.querySelectorAll(".chip").forEach(ch => {
ch.onclick = () => {
const act = ch.dataset.act, v = ch.dataset.v;
if(act === "cond") sendMsg({ key: COMMANDS.setOpCondition.key, value: {state_uint8: Number(v)} });
else if(act === "mode") sendMsg({ key: COMMANDS.setOpMode.key, value: {state_uint8: Number(v)} });
else if(act === "power") sendMsg({ key: COMMANDS.setPowerSystem.key, value: {powerCmd_uint8: Number(v)} });
else if(act === "task") sendMsg({ key: COMMANDS.taskStart.key, value: {taskType_str: v, balanceEnabled_bool: true, bowRudderUnfold_bool: true} });
};
});
el("btnDev").onclick = () => {
sendMsg({ key: COMMANDS.setDeviceSwitch.key, value: { id_uint8: Number(el("devSel").value), cmd_uint8: Number(el("devCmd").value) } });
};
}
function escapeHtml(s){ return s.replace(/[&<>]/g, c => ({"&":"&amp;","<":"&lt;",">":"&gt;"}[c])); }
function renderMonitor(snap){
const keys = Object.keys(snap).sort();
el("monitorMeta").textContent = "收到 " + keys.length + " 个反馈变量 · " + new Date().toLocaleTimeString();
const box = el("monitor");
box.innerHTML = "";
keys.forEach(k => {
const raw = snap[k];
let pretty = raw;
try { pretty = JSON.stringify(JSON.parse(raw), null, 2); } catch(e){}
const d = document.createElement("details");
d.className = "var-item";
d.innerHTML = `<summary>${k}</summary><pre>${escapeHtml(pretty)}</pre>`;
box.appendChild(d);
});
}
// ---- 连续数据曲线 ----
function collectNumbers(node, prefix, out){
if (Array.isArray(node)) return;
if (typeof node === "number") { out[prefix] = node; return; }
if (node && typeof node === "object") {
for (const k in node) collectNumbers(node[k], prefix + "." + k, out);
}
}
function onSnapshot(snap){
renderMonitor(snap);
const now = Date.now();
const nums = {};
for (const key in snap) {
let obj; try { obj = JSON.parse(snap[key]); } catch(e){ continue; }
collectNumbers(obj, key, nums);
}
for (const p in nums) allFields.add(p);
for (const p in history) {
if (p in nums) {
history[p].points.push([now, nums[p]]);
if (history[p].points.length > MAX_POINTS) history[p].points.shift();
}
}
refreshFieldOptions();
drawChart();
}
function refreshFieldOptions(){
const sel = el("fieldSel");
const prev = sel.value;
const arr = Array.from(allFields).sort();
sel.innerHTML = arr.map(p => `<option value="${p}">${p}</option>`).join("");
if (prev && arr.indexOf(prev) >= 0) sel.value = prev;
}
function addSeries(path){
if (!path || history[path]) return;
history[path] = { color: COLORS[nextColor % COLORS.length], points: [] };
nextColor++;
renderSeriesBox();
drawChart();
}
function removeSeries(path){
delete history[path];
renderSeriesBox();
drawChart();
}
function renderSeriesBox(){
const box = el("seriesBox");
const paths = Object.keys(history);
if (!paths.length) { box.innerHTML = '<span class="muted">未添加曲线</span>'; return; }
box.innerHTML = paths.map(p =>
`<span class="serie"><span class="swatch" style="background:${history[p].color}"></span>${p}` +
`<span class="rm" data-path="${p}">×</span></span>`
).join("");
box.querySelectorAll(".rm").forEach(x => { x.onclick = () => removeSeries(x.dataset.path); });
}
function fmt(v){ return Math.abs(v) >= 1000 ? v.toFixed(0) : (Math.abs(v) >= 10 ? v.toFixed(1) : v.toFixed(2)); }
function fmtTime(t){ const d = new Date(t); return d.toLocaleTimeString(); }
function drawChart(){
const canvas = el("chart");
const dpr = window.devicePixelRatio || 1;
const w = canvas.clientWidth || 1000;
const h = canvas.clientHeight || 280;
canvas.width = Math.floor(w * dpr);
canvas.height = Math.floor(h * dpr);
const ctx = canvas.getContext("2d");
ctx.setTransform(dpr,0,0,dpr,0,0);
ctx.fillStyle = "#0b1220"; ctx.fillRect(0,0,w,h);
const series = Object.keys(history).filter(k => history[k].points.length >= 1);
if (!series.length){
ctx.fillStyle = "#64748b"; ctx.font = "13px sans-serif"; ctx.textAlign = "center";
ctx.fillText("添加曲线后显示连续数据趋势", w/2, h/2);
return;
}
let minT=Infinity, maxT=-Infinity, minV=Infinity, maxV=-Infinity;
series.forEach(k => history[k].points.forEach(([t,v])=>{
if(t<minT)minT=t; if(t>maxT)maxT=t; if(v<minV)minV=v; if(v>maxV)maxV=v;
}));
if(minV === maxV){ minV -= 1; maxV += 1; }
if(minT === maxT){ minT -= 500; maxT += 500; }
const pad = {l:52,r:12,t:12,b:26};
const pw = w - pad.l - pad.r, ph = h - pad.t - pad.b;
const X = t => pad.l + (t-minT)/(maxT-minT)*pw;
const Y = v => pad.t + (1-(v-minV)/(maxV-minV))*ph;
ctx.strokeStyle = "#1e293b"; ctx.fillStyle = "#94a3b8"; ctx.font = "10px sans-serif";
for(let i=0;i<=4;i++){
const val = minV + (maxV-minV)*i/4, yy = Y(val);
ctx.textAlign = "right"; ctx.fillText(fmt(val), pad.l-6, yy+3);
ctx.beginPath(); ctx.moveTo(pad.l, yy); ctx.lineTo(w-pad.r, yy); ctx.stroke();
}
for(let i=0;i<=4;i++){
const tt = minT + (maxT-minT)*i/4, xx = X(tt);
ctx.textAlign = "center"; ctx.fillText(fmtTime(tt), xx, h-pad.b+14);
ctx.beginPath(); ctx.moveTo(xx, pad.t); ctx.lineTo(xx, h-pad.b); ctx.stroke();
}
series.forEach(k => {
const {color, points} = history[k];
ctx.strokeStyle = color; ctx.lineWidth = 1.5; ctx.beginPath();
points.forEach(([t,v],i)=>{ const xx=X(t), yy=Y(v); if(i)ctx.lineTo(xx,yy); else ctx.moveTo(xx,yy); });
ctx.stroke();
});
}
// 初始化
function init(){
const sel = el("cmdType");
Object.keys(COMMANDS).forEach(k => {
const o = document.createElement("option");
o.value = k; o.textContent = COMMANDS[k].label;
sel.appendChild(o);
});
sel.value = currentCmd;
applyTemplate(currentCmd);
sel.onchange = () => { currentCmd = sel.value; applyTemplate(currentCmd); };
el("btnSend").onclick = sendCurrent;
el("btnReset").onclick = () => applyTemplate(currentCmd);
el("btnRawSend").onclick = () => {
const k = el("rawKey").value.trim();
const v = el("rawValue").value.trim();
if(!k){ log("变量名为空", "err"); return; }
sendMsg({ key: k, value: v });
};
el("btnAddSeries").onclick = () => addSeries(el("fieldSel").value);
el("btnClearSeries").onclick = () => { history = {}; nextColor = 0; renderSeriesBox(); drawChart(); };
window.addEventListener("resize", drawChart);
buildQuick();
renderSeriesBox();
drawChart();
connect();
}
init();
</script>
</body>
</html>
Binary file not shown.
File diff suppressed because it is too large Load Diff
+8
View File
@@ -2,6 +2,13 @@
// DriverTest: Driver 类的设备表生成(覆盖全部 10 个工况)、设备状态读写、 // DriverTest: Driver 类的设备表生成(覆盖全部 10 个工况)、设备状态读写、
// JSON 序列化/反序列化。 // JSON 序列化/反序列化。
// 不改动生产代码;仅链接 src/pPowerManger/driver.cpp。 // 不改动生产代码;仅链接 src/pPowerManger/driver.cpp。
//
// 测试目的:
// 1. 验证每种工况模式下,驱动表生成是否符合预期的设备开关状态
// 2. 验证设备状态的读写操作(setDeviceState / getDeviceState)
// 3. 验证驱动表的 JSON 序列化/反序列化是否能正确往返
// 4. 验证 JSON 解析为设备命令的逻辑
// 5. 验证安全读取无符号整数的边界处理
//======================================================================= //=======================================================================
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <cstring> #include <cstring>
@@ -10,6 +17,7 @@
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
// 全字段比较宏(62 个设备字段) // 全字段比较宏(62 个设备字段)
// 用于比较两个 DriverTable 结构体的所有字段是否完全相等
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
#define EXPECT_TABLE_EQ(a, b) do { \ #define EXPECT_TABLE_EQ(a, b) do { \
EXPECT_EQ((a).depth1,(b).depth1); EXPECT_EQ((a).depth2,(b).depth2); \ EXPECT_EQ((a).depth1,(b).depth1); EXPECT_EQ((a).depth2,(b).depth2); \