#!/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}" </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