构建:修复 GTest 链接失败——显式指定 Linux GTest 配置,避免从 PATH 探测到 Windows Anaconda 的 .lib
This commit is contained in:
@@ -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/
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
+1240
File diff suppressed because it is too large
Load Diff
Executable
+306
@@ -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
|
||||||
@@ -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
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 ®ISTRY[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)
|
||||||
@@ -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 下发布)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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 => ({"&":"&","<":"<",">":">"}[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
@@ -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); \
|
||||||
|
|||||||
Reference in New Issue
Block a user