新增 pMotor 推进电机操作程序:经 pCanBridge(CAN1) 与电机控制器交互
build-test-deploy / ci (push) Failing after 32m37s

- 协议模块(protocol/MotorCan):12 条 CAN ID、指令帧 0x18EF2010 编码、
  状态帧(转速/母线电压/温度)与支路一/二故障报警帧解码,故障位按协议
  逐位映射中文描述(docs/推进电机通信协议20260321.docx)
- 主程序(MOOS 应用):订阅 pCanBridge 透传 CAN_0x* 解码电机状态;
  500ms 周期下发指令帧(CAN_TX_0x18EF2010,m_sSrcAux=CAN1),
  可选 red_channel 冗余下发 0x18EF2011;复位位保持 reset_hold_sec
- 网页(18081):按协议指令帧字段独立下发(使能命令 Byte0 bit0 /
  复位命令 Byte0 bit1 / 期望转速 Byte2/3,无联动按钮);
  WebSocket 1Hz 推送转速/电压/温度/故障告警/链路统计
- 配置接入:missions/h100.moos 增加 pMotor 配置块(can_channel=CAN1)
- 测试:pmotorTest 92 项(指令帧字节级对照协议示例、解码/故障表)
- 部署:scripts/build-board.sh 增加 pMotor 产物拷贝 + pMotor.service;
  .gitignore 补 bin/pMotor、bin/pmotorTest
- 联调验证:MOOSDB+pCanBridge+假CANET 全链路,下行帧 01 00 E8 03
  与协议文档示例一致,复位位 3s 保持行为正确
This commit is contained in:
zjk
2026-09-02 10:50:28 +08:00
parent 67e732045a
commit 873a1a9b09
20 changed files with 2007 additions and 10 deletions
+2
View File
@@ -252,6 +252,7 @@ bin/pPowerMangerHost
bin/pCCU
bin/pccuTest
bin/pCanBridge
bin/pMotor
# CI test reports
reports/
@@ -260,6 +261,7 @@ reports/
bin/PowerMangerTests
bin/udpFeeder
bin/fullFeeder
bin/pmotorTest
# ============================================================
# 数据库与日志文件(全局忽略,一律不入库)
Binary file not shown.
+28 -5
View File
@@ -1,6 +1,6 @@
// MOOS file
// 板卡 (RK3588 / Ubuntu 20.04) 专用 mission 配置
// 五个 systemd 服务(moosdb / pPowerManger / pPowerMangerHost / pCCU / pCanBridge)
// 六个 systemd 服务(moosdb / pPowerManger / pPowerMangerHost / pCCU / pCanBridge / pMotor)
// 共用本文件。
// 由 scripts/deploy.sh 推送到板卡 /root/work/h100/missions/h100.moos
@@ -87,11 +87,34 @@ ProcessConfig = pCanBridge
// CAN<->MOOSDB 多通道透传:CANET(TCP Server 模式)
// 每通道一条独立 TCP 连接;工作端口:CAN0=4001,CAN1=4002,...,CAN7=4008
channel = CAN0,192.168.0.222,4001
channel = CAN1,192.168.0.222,4002
// channel = CAN1,192.168.0.222,4002
// CAN_TX 下行默认通道(pCCU 未指定 m_sSrcAux 时使用;可省略)
default_channel = CAN0
// CAN_TX 下行默认通道(pCCU 未指定 m_sSrcAux 时使用;可省略)
default_channel = CAN0
// CAN 帧 SQLite 落库路径(can_frame 表,channel 列区分通道)
dbpath = /root/work/h100/data/pCanBridge_data.db
// CAN 帧 SQLite 落库路径(can_frame 表,channel 列区分通道)
dbpath = /root/work/h100/data/pCanBridge_data.db
}
ProcessConfig = pMotor
{
AppTick = 4
CommsTick = 4
// 推进电机控制器:CAN1 通道(经 pCanBridge 透传,docs/推进电机通信协议20260321.docx)
// 下行 500ms 周期指令帧 0x18EF2010(网页 /api/motor_cmd 控制启停/复位/转速),
// 上行订阅 CAN_0x* 解码故障/转速/母线电压/温度,网页 18081 展示
can_channel = CAN1
// 冗余 CAN 通道(配置后指令帧同内容发 0x18EF2011;留空=不发送)
// red_channel = CAN2
cmd_period_ms = 500
reset_hold_sec = 5
web_port = 18081
web_enable = true
logpath = /root/work/h100/data/pMotor.log
}
+28 -4
View File
@@ -7,7 +7,7 @@
#
# 用法:
# ./scripts/build-board.sh # 同步+编译+部署(不重启)
# ./scripts/build-board.sh --start # 编译后重启全部服务(含 pCanBridge)
# ./scripts/build-board.sh --start # 编译后重启全部服务(含 pCanBridge/pMotor)
# ./scripts/build-board.sh --clean # 板卡上重新 cmake 再编译
# ./scripts/build-board.sh --jobs 4 # 指定并行度(默认板卡 nproc)
#
@@ -68,7 +68,7 @@ DO_CLEAN=0
CCU_HOST=""
CCU_PORT=""
SERVICES=(moosdb pPowerManger pPowerMangerHost pCCU pCanBridge)
SERVICES=(moosdb pPowerManger pPowerMangerHost pCCU pCanBridge pMotor)
info() { printf '\033[1;36m[build-board]\033[0m %s\n' "$*"; }
ok() { printf '\033[1;32m[build-board]\033[0m %s\n' "$*"; }
@@ -211,12 +211,13 @@ ${SSH} "${SSH_TARGET}" "mkdir -p ${BOARD_BIN} && \
cp -f ${BOARD_SRC}/bin/pPowerMangerHost ${BOARD_BIN}/pPowerMangerHost && \
cp -f ${BOARD_SRC}/bin/pCCU ${BOARD_BIN}/pCCU && \
cp -f ${BOARD_SRC}/bin/pCanBridge ${BOARD_BIN}/pCanBridge && \
chmod +x ${BOARD_BIN}/pPowerManger ${BOARD_BIN}/pPowerMangerHost ${BOARD_BIN}/pCCU ${BOARD_BIN}/pCanBridge" \
cp -f ${BOARD_SRC}/bin/pMotor ${BOARD_BIN}/pMotor && \
chmod +x ${BOARD_BIN}/pPowerManger ${BOARD_BIN}/pPowerMangerHost ${BOARD_BIN}/pCCU ${BOARD_BIN}/pCanBridge ${BOARD_BIN}/pMotor" \
|| { err "部署产物失败"; exit 1; }
ok "产物已部署"
#-------------------------------------------------------------------
# 4.5 安装/刷新 pCanBridge systemd unit(幂等,其它 4 个 unit 已装)
# 4.5 安装/刷新 pCanBridge systemd unit(幂等,其它 5 个 unit 已装)
#-------------------------------------------------------------------
info "安装 pCanBridge.service ..."
${SSH} "${SSH_TARGET}" "cat > /etc/systemd/system/pCanBridge.service" <<EOF
@@ -238,6 +239,29 @@ EOF
${SSH} "${SSH_TARGET}" "systemctl daemon-reload && systemctl reset-failed pCanBridge 2>/dev/null || true"
ok "pCanBridge.service 已安装"
#-------------------------------------------------------------------
# 4.6 安装/刷新 pMotor systemd unit(幂等)
#-------------------------------------------------------------------
info "安装 pMotor.service ..."
${SSH} "${SSH_TARGET}" "cat > /etc/systemd/system/pMotor.service" <<EOF
[Unit]
Description=pMotor Propulsion Motor for H100 Power Manager
After=moosdb.service pCanBridge.service
Requires=moosdb.service
[Service]
Type=simple
WorkingDirectory=${BOARD_BIN}
ExecStart=${BOARD_BIN}/pMotor --alias=pMotor /root/work/h100/missions/h100.moos
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
${SSH} "${SSH_TARGET}" "systemctl daemon-reload && systemctl reset-failed pMotor 2>/dev/null || true"
ok "pMotor.service 已安装"
echo ""
ok "产物: ${BOARD_BIN}/pPowerManger (aarch64)"
${SSH} "${SSH_TARGET}" "file ${BOARD_BIN}/pPowerManger | cut -c1-60"
+1
View File
@@ -22,6 +22,7 @@ add_subdirectory(pPMtest)
add_subdirectory(pPowerMangerHost)
add_subdirectory(pCCU)
add_subdirectory(pCanBridge)
add_subdirectory(pMotor)
##############################################################################
# END of CMakeLists.txt
##############################################################################
+46
View File
@@ -0,0 +1,46 @@
#--------------------------------------------------------
# The CMakeLists.txt for: pMotor
# 推进电机操作程序:网页控制 <-> pCanBridge(CAN1) <-> 电机控制器
#--------------------------------------------------------
if (${WIN32})
SET(SYSTEM_LIBS wsock32)
else (${WIN32})
SET(SYSTEM_LIBS m pthread)
endif (${WIN32})
# 复用仓库内 pPowerManger 的公共源码(相对路径引用,避免重复维护)
SET(PM_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../pPowerManger)
SET(SHARED_SRC
${PM_DIR}/logc/loguru.cpp
${PM_DIR}/httpserver/mongoose.c
)
SET(SRC
${SHARED_SRC}
protocol/MotorCan.cpp
web/WebServer.cpp
Motor.cpp
Motor_Info.cpp
main.cpp
)
ADD_EXECUTABLE(pMotor ${SRC})
TARGET_INCLUDE_DIRECTORIES(pMotor PRIVATE
${PM_DIR}
${PM_DIR}/httpserver
${PM_DIR}/logc
)
TARGET_LINK_LIBRARIES(pMotor
${MOOS_LIBRARIES}
apputil
mbutil
m
pthread
jsoncpp
dl
${SYSTEM_LIBS}
)
+467
View File
@@ -0,0 +1,467 @@
#include "Motor.h"
#include "MBUtils.h"
#include "../pPowerManger/logc/loguru.hpp"
#include "json/json.h"
#include <cstdio>
#include <sstream>
using namespace motor;
using namespace std;
//---------------------------------------------------------
// Constructor / Destructor
Motor::Motor() {}
Motor::~Motor() {
if (m_web) { m_web->stop(); delete m_web; m_web = nullptr; }
}
//---------------------------------------------------------
// OnStartUp:读取配置并初始化各组件
bool Motor::OnStartUp() {
AppCastingMOOSApp::OnStartUp();
STRING_LIST sParams;
m_MissionReader.EnableVerbatimQuoting(false);
if (!m_MissionReader.GetConfiguration(GetAppName(), sParams))
reportConfigWarning("No config block found for " + GetAppName());
STRING_LIST::iterator p;
for (p = sParams.begin(); p != sParams.end(); p++) {
string orig = *p;
string line = *p;
string param = stripBlankEnds(tolower(biteStringX(line, '=')));
string value = stripBlankEnds(line);
bool handled = true;
if (param == "can_channel") m_canChannel = value;
else if (param == "red_channel") m_redChannel = value;
else if (param == "cmd_period_ms") m_cmdPeriodMs = atoi(value.c_str());
else if (param == "reset_hold_sec") m_resetHoldSec = atof(value.c_str());
else if (param == "web_port") m_webPort = atoi(value.c_str());
else if (param == "web_enable") m_webEnable = (tolower(value) == "true" || value == "1");
else if (param == "logpath") m_logPath = value;
else handled = false;
if (!handled)
reportUnhandledConfigWarning(orig);
}
if (m_cmdPeriodMs < 50) m_cmdPeriodMs = 50;
registerVariables();
// 日志文件
if (m_logPath.empty()) m_logPath = "pMotor.log";
loguru::add_file(m_logPath.c_str(), loguru::Append, loguru::Verbosity_MAX);
LOG_F(INFO, "pMotor log path: %s", m_logPath.c_str());
// 网页
if (m_webEnable) {
m_web = new WebServer();
m_web->setOnOpen([this]() { return buildSnapshot(); });
m_web->setApiHandler([this](const std::string& uri, const std::string& query) {
return handleApi(uri, query);
});
if (!m_web->start(m_webPort)) {
LOG_F(ERROR, "web server start failed on port %d", m_webPort);
delete m_web;
m_web = nullptr;
}
}
LOG_F(INFO, "pMotor started: motor CAN via pCanBridge channel=%s (red_channel=%s), "
"cmd period=%dms, web=%s",
m_canChannel.c_str(), m_redChannel.c_str(), m_cmdPeriodMs,
m_web ? ("port " + to_string(m_webPort)).c_str() : "disabled");
return true;
}
//---------------------------------------------------------
// OnConnectToServer
bool Motor::OnConnectToServer() {
registerVariables();
return true;
}
//---------------------------------------------------------
// registerVariables:订阅 pCanBridge 透传的全部 CAN 帧
void Motor::registerVariables() {
AppCastingMOOSApp::RegisterVariables();
// 通配订阅必须用三参数重载(变量模式 + 来源模式)
Register("CAN_0x*", "*", 0);
}
//---------------------------------------------------------
// OnNewMail
bool Motor::OnNewMail(MOOSMSG_LIST &NewMail) {
AppCastingMOOSApp::OnNewMail(NewMail);
MOOSMSG_LIST::iterator p;
for (p = NewMail.begin(); p != NewMail.end(); p++) {
CMOOSMsg &msg = *p;
if (msg.m_sKey.rfind("CAN_0x", 0) == 0)
handleCanMessage(msg);
}
return true;
}
//---------------------------------------------------------
// handleCanMessage:解码 pCanBridge 透传的 CAN 帧
//
// 消息格式(pCanBridge 约定):
// m_sKey = "CAN_0x%08X"(CAN ID,扩展帧)
// m_sVal = 二进制数据域(8 字节)
// m_dfVal2 = 原始帧信息字节(bit7 FF / bit6 RTR / bit3~0 DLC)
void Motor::handleCanMessage(CMOOSMsg& msg) {
// 从变量名解析 CAN ID(跳过前缀 "CAN_0x" 共 6 字符)
uint32_t canId = 0;
if (std::sscanf(msg.m_sKey.c_str() + 6, "%x", &canId) != 1) return;
if (!isCanMotorId(canId)) return;
if (!(msg.IsBinary() && msg.GetBinaryDataSize() >= 8)) return;
const unsigned char* d = msg.GetBinaryData();
if (!d) return;
// 日志值在锁内取副本,避免与 Web 线程快照读取竞争
uint32_t rx = 0;
int spd = 0;
int bus = 0;
size_t f1 = 0, f2 = 0;
{
std::lock_guard<std::mutex> lock(m_mutex);
if (!decodeMotorFrame(canId, d, static_cast<int>(msg.GetBinaryDataSize()), m_state))
return;
m_state.rxCount++;
double now = MOOSTime(false);
switch (canId) {
case MOTOR_CAN_FAULT1: case MOTOR_CAN_FAULT1_RED:
m_state.branch1.lastRxTime = now; break;
case MOTOR_CAN_FAULT2: case MOTOR_CAN_FAULT2_RED:
m_state.branch2.lastRxTime = now; break;
case MOTOR_CAN_STATE3: case MOTOR_CAN_STATE3_RED:
m_state.state3LastRx = now; break;
case MOTOR_CAN_STATE4: case MOTOR_CAN_STATE4_RED:
m_state.state4LastRx = now; break;
default: break;
}
rx = m_state.rxCount;
spd = m_state.feedbackSpeed;
bus = m_state.busVoltage;
f1 = m_state.branch1.faults.size();
f2 = m_state.branch2.faults.size();
}
// 节流日志:1s 一条
double now = MOOSTime();
if (now - m_lastRxLog >= 1.0) {
m_lastRxLog = now;
LOG_F(INFO, "[Motor/CAN] rx=%u speed=%d rpm bus=%dV faults(b1=%zu,b2=%zu)",
rx, spd, bus, f1, f2);
}
}
//---------------------------------------------------------
// Iterate:周期任务(AppTick 次/秒)
bool Motor::Iterate() {
AppCastingMOOSApp::Iterate();
double now = MOOSTime();
// 网页指令出队并应用
processCmdQueue();
// 周期发送指令帧(500ms,协议要求)
sendCmdFrame(now);
// 网页快照 1Hz 推送
if (m_web) {
static double lastWebPush = 0;
if (now - lastWebPush >= 1.0) {
lastWebPush = now;
m_web->broadcast(buildSnapshot());
}
}
AppCastingMOOSApp::PostReport();
return true;
}
//---------------------------------------------------------
// handleApi:/api/motor_cmd
std::string Motor::handleApi(const std::string& uri, const std::string& query) {
if (uri == "/api/motor_cmd")
return handleMotorCmdApi(query);
return "";
}
//---------------------------------------------------------
// handleMotorCmdApi:电机控制指令入队(按协议指令帧字段独立下发,无联动)
//
// GET /api/motor_cmd?action=enable 使能命令=1(Byte0 bit0)
// GET /api/motor_cmd?action=disable 使能命令=0,期望转速清0(Byte0 bit0,协议"停机"示例)
// GET /api/motor_cmd?action=reset 复位命令=1(Byte0 bit1,仅故障态有效)
// GET /api/motor_cmd?action=speed&speed=1000 设定期望转速(Byte2/3,s16)
// Web 线程不可直接 Notify,指令入队后由 Iterate(MOOS 线程)应用。
std::string Motor::handleMotorCmdApi(const std::string& query) {
// 简易 query 解析(k=v&k=v)
auto getParam = [&query](const char* key, std::string& out) -> bool {
std::string k = std::string(key) + "=";
size_t p = query.find(k);
if (p == std::string::npos) return false;
size_t e = query.find('&', p);
out = query.substr(p + k.size(),
(e == std::string::npos) ? std::string::npos
: e - p - k.size());
return true;
};
std::string action, speedStr;
getParam("action", action);
getParam("speed", speedStr);
MotorCmd c;
if (action == "enable") {
c.action = MOTOR_ACT_ENABLE;
} else if (action == "disable") {
c.action = MOTOR_ACT_DISABLE;
} else if (action == "reset") {
c.action = MOTOR_ACT_RESET;
} else if (action == "speed") {
c.action = MOTOR_ACT_SPEED;
c.speed = static_cast<int16_t>(atoi(speedStr.c_str()));
} else {
return "{\"ok\":false,\"error\":\"bad action (enable/disable/reset/speed)\"}";
}
{
std::lock_guard<std::mutex> lock(m_mutex);
if (m_cmdQueue.size() >= 16)
return "{\"ok\":false,\"error\":\"queue full\"}";
m_cmdQueue.push_back(c);
m_lastCmd = c;
m_lastCmdValid = true;
m_lastCmdTime = MOOSTime(false);
}
LOG_F(INFO, "[Motor/CAN] 指令入队: action=%s speed=%d", action.c_str(), c.speed);
char buf[96];
std::snprintf(buf, sizeof(buf),
"{\"ok\":true,\"action\":\"%s\",\"speed\":%d}", action.c_str(), c.speed);
return buf;
}
//---------------------------------------------------------
// processCmdQueue:出队网页指令并更新指令状态
void Motor::processCmdQueue() {
MotorCmd c;
double now = MOOSTime();
{
std::lock_guard<std::mutex> lock(m_mutex);
while (!m_cmdQueue.empty()) {
c = m_cmdQueue.front();
m_cmdQueue.erase(m_cmdQueue.begin());
switch (c.action) {
case MOTOR_ACT_ENABLE:
m_enable = true;
break;
case MOTOR_ACT_DISABLE:
m_enable = false;
m_speed = 0;
m_resetUntil = 0;
break;
case MOTOR_ACT_SPEED:
m_speed = c.speed;
break;
case MOTOR_ACT_RESET:
m_resetUntil = now + m_resetHoldSec;
break;
default:
break;
}
LOG_F(INFO, "[Motor/CAN] 指令应用: action=%d speed=%d enable=%d",
c.action, m_speed, m_enable ? 1 : 0);
}
}
}
//---------------------------------------------------------
// sendCmdFrame:按周期编码并发送指令帧
// 主通道 0x18EF2010;配置 red_channel 时同帧发 0x18EF2011 到冗余通道
void Motor::sendCmdFrame(double now) {
double lastTx = 0;
{
std::lock_guard<std::mutex> lock(m_mutex);
if (now - m_lastTx < m_cmdPeriodMs / 1000.0)
return;
lastTx = m_lastTx;
}
if (now - lastTx < m_cmdPeriodMs / 1000.0)
return;
uint8_t data[8];
{
std::lock_guard<std::mutex> lock(m_mutex);
bool reset = (now < m_resetUntil);
buildMotorCmdData(m_enable, reset, m_enable ? m_speed : 0, data);
m_lastTx = now;
}
bool ok = postCanFrame(MOTOR_CAN_CMD, data, m_canChannel);
int okCount = ok ? 1 : 0, errCount = ok ? 0 : 1;
if (!m_redChannel.empty()) {
bool okRed = postCanFrame(MOTOR_CAN_CMD_RED, data, m_redChannel);
okCount += okRed ? 1 : 0;
errCount += okRed ? 0 : 1;
}
{
std::lock_guard<std::mutex> lock(m_mutex);
m_txCount += static_cast<unsigned long>(okCount);
m_txErr += static_cast<unsigned long>(errCount);
}
}
//---------------------------------------------------------
// postCanFrame:MOOS CAN_TX_0x%08X(二进制数据域,m_sSrcAux=目标通道)
// 链路:MOOS -> pCanBridge 订阅 -> 按通道路由 CanEndpoint(TCP)
// -> CANET -> CAN 总线
bool Motor::postCanFrame(uint32_t canId, const uint8_t data[8], const std::string& channel) {
char key[32];
std::snprintf(key, sizeof(key), "CAN_TX_0x%08X", canId);
CMOOSMsg msg(MOOS_NOTIFY, key, 8u, data);
msg.SetSourceAux(channel);
bool ok = m_Comms.Post(msg);
if (!ok)
LOG_F(ERROR, "[Motor/CAN] 指令帧 0x%08X 投递 MOOS 失败 (通道=%s)", canId, channel.c_str());
return ok;
}
//---------------------------------------------------------
// buildSnapshot:网页 JSON 快照(jsoncpp)
namespace {
void putI(Json::Value& j, const char* k, int v) { j[k] = Json::Value(v); }
void putU(Json::Value& j, const char* k, unsigned long v) { j[k] = Json::Value(static_cast<Json::UInt64>(v)); }
void putD(Json::Value& j, const char* k, double v) { j[k] = Json::Value(v); }
// age:距最后收到该帧的秒数;valid=false 时前端显示"从未收到"
void putAge(Json::Value& j, double lastRx, double now, bool valid) {
j["valid"] = valid ? 1 : 0;
j["age"] = valid ? (now - lastRx) : -1;
}
Json::Value branchToJson(const MotorBranchState& br, double now) {
Json::Value j(Json::objectValue);
putAge(j, br.lastRxTime, now, br.valid);
Json::Value words(Json::arrayValue);
for (int i = 0; i < 4; ++i) words.append(static_cast<int>(br.word[i]));
j["words"] = words;
Json::Value faults(Json::arrayValue);
for (size_t i = 0; i < br.faults.size(); ++i) faults.append(br.faults[i]);
j["faults"] = faults;
Json::Value alarms(Json::arrayValue);
for (size_t i = 0; i < br.alarms.size(); ++i) alarms.append(br.alarms[i]);
j["alarms"] = alarms;
return j;
}
} // namespace
std::string Motor::buildSnapshot() {
Json::Value root(Json::objectValue);
double now = MOOSTime(false);
{
std::lock_guard<std::mutex> lock(m_mutex);
// 通信链路
Json::Value link(Json::objectValue);
link["channel"] = m_canChannel;
link["redChannel"] = m_redChannel;
link["periodMs"] = m_cmdPeriodMs;
putU(link, "txCount", m_txCount);
putU(link, "txErr", m_txErr);
putU(link, "rxCount", m_state.rxCount);
link["lastTxValid"] = (m_lastTx > 0) ? 1 : 0;
putD(link, "lastTxAge", m_lastTx > 0 ? (now - m_lastTx) : -1);
root["link"] = link;
// 指令状态
Json::Value cmd(Json::objectValue);
putI(cmd, "enable", m_enable ? 1 : 0);
putI(cmd, "speed", m_speed);
putI(cmd, "resetActive", (now < m_resetUntil) ? 1 : 0);
cmd["lastCmdValid"] = m_lastCmdValid ? 1 : 0;
if (m_lastCmdValid) {
static const char* kActionName[] = {"", "enable", "disable", "speed", "reset"};
int idx = (m_lastCmd.action >= 1 && m_lastCmd.action <= 4) ? m_lastCmd.action : 0;
cmd["lastCmdAction"] = kActionName[idx];
putI(cmd, "lastCmdSpeed", m_lastCmd.speed);
putD(cmd, "lastCmdAge", m_lastCmdTime > 0 ? (now - m_lastCmdTime) : -1);
}
root["cmd"] = cmd;
// 状态帧3:转速 / 母线电压
Json::Value s3(Json::objectValue);
putAge(s3, m_state.state3LastRx, now, m_state.state3Valid);
putI(s3, "speed", m_state.feedbackSpeed);
putI(s3, "voltage", m_state.busVoltage);
root["state3"] = s3;
// 状态帧4:温度
Json::Value s4(Json::objectValue);
putAge(s4, m_state.state4LastRx, now, m_state.state4Valid);
putI(s4, "tA", m_state.tempA);
putI(s4, "tB", m_state.tempB);
putI(s4, "tC", m_state.tempC);
putI(s4, "tInv", m_state.tempInv);
putI(s4, "tFilter", m_state.tempFilter);
putI(s4, "tAir", m_state.tempAir);
root["state4"] = s4;
// 故障/报警
root["fault1"] = branchToJson(m_state.branch1, now);
root["fault2"] = branchToJson(m_state.branch2, now);
}
Json::FastWriter writer;
return writer.write(root);
}
//---------------------------------------------------------
// buildReport
bool Motor::buildReport() {
m_msgs << "============================================" << "\n";
m_msgs << "pMotor 推进电机操作程序" << "\n";
m_msgs << "============================================" << "\n";
m_msgs << "CAN 通道: " << m_canChannel
<< (m_redChannel.empty() ? "" : (" 冗余: " + m_redChannel)).c_str()
<< " 指令周期: " << m_cmdPeriodMs << "ms" << "\n";
m_msgs << "指令帧下发: " << m_txCount << " 成功 / " << m_txErr << " 失败" << "\n";
{
std::lock_guard<std::mutex> lock(m_mutex);
m_msgs << "电机报文接收: " << m_state.rxCount << "\n";
m_msgs << "指令状态: enable=" << (m_enable ? 1 : 0)
<< " speed=" << m_speed
<< " reset=" << (MOOSTime(false) < m_resetUntil ? 1 : 0) << "\n";
m_msgs << "反馈转速: " << m_state.feedbackSpeed
<< " rpm 母线电压: " << m_state.busVoltage << "\n";
m_msgs << "支路一故障: " << m_state.branch1.faults.size()
<< " 条, 支路二故障: " << m_state.branch2.faults.size() << " 条" << "\n";
}
return true;
}
+96
View File
@@ -0,0 +1,96 @@
#ifndef PMOTOR_MOTOR_H
#define PMOTOR_MOTOR_H
#define UNIX
#include "MOOS/libMOOS/Thirdparty/AppCasting/AppCastingMOOSApp.h"
#include "protocol/MotorCan.h"
#include "web/WebServer.h"
#include <mutex>
namespace motor {
//============================================================================
// Motor:pMotor 程序主类(MOOS 应用外壳)——推进电机操作程序。
//
// 数据流:
// 上行:电机控制器 -> CAN1 -> CANET -> pCanBridge -> MOOS CAN_0x*
// -> pMotor 订阅解码(故障/转速/母线电压/温度)-> SystemData 合成快照
// 下行:网页 /api/motor_cmd(启停/复位/设定转速)
// -> 指令入队 -> Iterate(MOOS 线程)按 500ms 周期编码指令帧
// -> MOOS CAN_TX_0x18EF2010(m_sSrcAux=CAN1)-> pCanBridge
// -> CANET(CAN1) -> 电机控制器
// 网页:WebSocket 周期推送 JSON 快照(1Hz)。
//
// 协议定义见 docs/推进电机通信协议20260321.docx。
//============================================================================
class Motor : public AppCastingMOOSApp {
public:
Motor();
~Motor();
protected:
bool OnNewMail(MOOSMSG_LIST &NewMail);
bool Iterate();
bool OnConnectToServer();
bool OnStartUp();
bool buildReport();
void registerVariables();
private:
// MOOS 订阅消息处理(CAN_0x* 由 pCanBridge 发布)
void handleCanMessage(CMOOSMsg& msg);
// 网页
std::string buildSnapshot();
std::string handleApi(const std::string& uri, const std::string& query);
// 控制指令:/api/motor_cmd 解析入队(Web 线程)
std::string handleMotorCmdApi(const std::string& query);
// 出队并应用控制指令(Iterate 调用)
void processCmdQueue();
// 周期(默认 500ms)发送指令帧 0x18EF2010(+可选冗余 0x18EF2011)
void sendCmdFrame(double now);
// 经 MOOS CAN_TX_0x%08X 发布一帧二进制数据(m_sSrcAux=通道名)
bool postCanFrame(uint32_t canId, const uint8_t data[8], const std::string& channel);
// 配置
std::string m_canChannel = "CAN1"; // 指令帧下行 / 帧上报来源通道
std::string m_redChannel; // 冗余 CAN 通道(空=不发送冗余帧)
int m_cmdPeriodMs = 500; // 指令帧周期
double m_resetHoldSec = 5.0; // 复位位保持时长(秒)
int m_webPort = 18081;
bool m_webEnable = true;
std::string m_logPath = "pMotor.log";
// 指令状态(processCmdQueue 修改,sendCmdFrame 读取;m_mutex 保护)
mutable std::mutex m_mutex;
bool m_enable = false;
int16_t m_speed = 0;
double m_resetUntil = 0; // 复位位有效的截止时间(MOOSTime)
// 指令队列(Web 线程入队,Iterate 出队)
std::vector<MotorCmd> m_cmdQueue;
// 最近一次网页指令(供网页展示)
MotorCmd m_lastCmd;
bool m_lastCmdValid = false;
double m_lastCmdTime = 0;
// 下行统计
unsigned long m_txCount = 0;
unsigned long m_txErr = 0;
double m_lastTx = 0;
// CAN 帧接收日志节流
double m_lastRxLog = 0;
// 电机状态(OnNewMail/Iterate 均在 MOOS 线程访问;buildSnapshot 经 m_mutex 读取)
MotorState m_state;
// 组件
WebServer* m_web = nullptr;
};
} // namespace motor
#endif // PMOTOR_MOTOR_H
+103
View File
@@ -0,0 +1,103 @@
/****************************************************************/
/* NAME: Motor_Info */
/* FILE: Motor_Info.cpp */
/****************************************************************/
#include <cstdlib>
#include <iostream>
#include "Motor_Info.h"
#include "ColorParse.h"
#include "ReleaseInfo.h"
using namespace std;
void showSynopsis() {
blk("SYNOPSIS: ");
blk("------------------------------------ ");
blk(" The pMotor application is the propulsion motor operation ");
blk(" program. It talks to the motor controller over CAN (via ");
blk(" pCanBridge pass-through, default channel CAN1): ");
blk(" subscribes motor state/fault frames (CAN_0x*), sends the ");
blk(" 500ms periodic command frame 0x18EF2010 (enable/reset/speed) ");
blk(" via MOOS CAN_TX_0x*, and provides a web page for control ");
blk(" and monitoring. Protocol: ");
blk(" docs/推进电机通信协议20260321.docx. ");
blk(" ");
}
void showHelpAndExit() {
blk(" ");
blu("=============================================================== ");
blu("Usage: pMotor file.moos [OPTIONS] ");
blu("=============================================================== ");
blk(" ");
showSynopsis();
blk(" ");
blk("Options: ");
mag(" --alias","=<ProcessName> ");
blk(" Launch pMotor with the given process name ");
blk(" rather than pMotor. ");
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 pMotor. ");
blk(" ");
blk("Note: If argv[2] does not otherwise match a known option, ");
blk(" then it will be interpreted as a run alias. This is ");
blk(" to support pAntler launching conventions. ");
blk(" ");
exit(0);
}
void showExampleConfigAndExit() {
blk(" ");
blu("=============================================================== ");
blu("pMotor Example MOOS Configuration ");
blu("=============================================================== ");
blk(" ");
blk("ProcessConfig = pMotor ");
blk("{ ");
blk(" AppTick = 4 ");
blk(" CommsTick = 4 ");
blk(" ");
blk(" can_channel = CAN1 // 电机 CAN 通道(pCanBridge 配置名) ");
blk(" red_channel = // 冗余 CAN 通道(空=不发冗余帧) ");
blk(" cmd_period_ms = 500 // 指令帧周期(协议 500ms) ");
blk(" reset_hold_sec= 5 // 复位位保持时长(秒) ");
blk(" web_port = 18081 // 网页端口(避开8080/8090/18080) ");
blk(" web_enable = true // 是否启用网页 ");
blk(" logpath = pMotor.log // 日志路径 ");
blk("} ");
blk(" ");
exit(0);
}
void showInterfaceAndExit() {
blk(" ");
blu("=============================================================== ");
blu("pMotor INTERFACE ");
blu("=============================================================== ");
blk(" ");
showSynopsis();
blk(" ");
blk("SUBSCRIPTIONS: ");
blk("------------------------------------ ");
blk(" CAN_0x* = pCanBridge 透传的 CAN 帧(电机状态/故障) ");
blk(" ");
blk("PUBLICATIONS: ");
blk("------------------------------------ ");
blk(" CAN_TX_0x18EF2010 = 主控指令帧(二进制 8 字节数据域, ");
blk(" m_sSrcAux=CAN1,经 pCanBridge 下行) ");
blk(" CAN_TX_0x18EF2011 = 指令帧冗余(配置 red_channel 时发送) ");
blk(" ");
exit(0);
}
void showReleaseInfoAndExit() {
showReleaseInfo("pMotor", "gpl");
exit(0);
}
+15
View File
@@ -0,0 +1,15 @@
/****************************************************************/
/* NAME: Motor_Info */
/* FILE: Motor_Info.h */
/****************************************************************/
#ifndef PMOTOR_INFO_HEADER
#define PMOTOR_INFO_HEADER
void showSynopsis();
void showHelpAndExit();
void showExampleConfigAndExit();
void showInterfaceAndExit();
void showReleaseInfoAndExit();
#endif
+54
View File
@@ -0,0 +1,54 @@
/************************************************************/
/* NAME: pMotor */
/* FILE: main.cpp */
/************************************************************/
#include <string>
#include "MBUtils.h"
#include "ColorParse.h"
#include "Motor.h"
#include "Motor_Info.h"
#include "../pPowerManger/logc/loguru.hpp"
using namespace std;
int main(int argc, char *argv[])
{
loguru::init(argc, argv);
string mission_file;
string run_command = argv[0];
// 默认以程序文件名(去掉路径)作为进程名,保证与 .moos 中 ProcessConfig 匹配
{
size_t slash = run_command.find_last_of('/');
if (slash != string::npos) run_command = run_command.substr(slash + 1);
}
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();
LOG_F(INFO, "pMotor launching as %s", run_command.c_str());
motor::Motor Motor;
Motor.Run(run_command.c_str(), mission_file.c_str());
return 0;
}
+37
View File
@@ -0,0 +1,37 @@
// MOOS file
// pMotor 推进电机操作程序 配置示例
//
// 功能:经 pCanBridge(CAN1 通道)与推进电机控制器交互
// 下行:网页 /api/motor_cmd(启停/复位/转速)-> 500ms 周期指令帧 0x18EF2010
// 上行:订阅 pCanBridge 透传的 CAN_0x*(故障/转速/母线电压/温度)-> 网页展示
//
// 协议定义:docs/推进电机通信协议20260321.docx
ServerHost = localhost
ServerPort = 9000
Community = h100
ProcessConfig = pMotor
{
AppTick = 4
CommsTick = 4
// 电机 CAN 通道(须与 pCanBridge 的 channel 配置名一致)
can_channel = CAN1
// 冗余 CAN 通道(配置后指令帧同内容发 0x18EF2011 到该通道;空=不发送)
// red_channel = CAN2
// 指令帧周期 ms(协议 500ms)
cmd_period_ms = 500
// 复位位保持时长(秒):下发复位后 reset bit 保持高电平数秒,
// 仅故障状态下电机执行复位
reset_hold_sec = 5
// 网页(避开 pCCU 8080 / pPowerManger 8090 / pPowerMangerHost 18080)
web_port = 18081
web_enable = true
logpath = pMotor.log
}
+219
View File
@@ -0,0 +1,219 @@
#include "MotorCan.h"
#include <cstring>
namespace motor {
const char* kMotorWordName[4] = {
"故障字1 (Byte0|Byte1)",
"故障字2 (Byte2|Byte3)",
"故障字3 (Byte4|Byte5)",
"报警字 (Byte6|Byte7)",
};
namespace {
// 故障/报警位描述(bit -> 中文描述),按字定义
struct FaultDesc {
uint16_t bit;
const char* text;
};
//--------------------------------------------------------------------------
// 故障字1(Byte0|Byte1):支路一 / 支路二 仅 bit7 不同
//--------------------------------------------------------------------------
const FaultDesc kW0Branch1[] = {
{0, "CPLD自检故障标志"},
{1, "温度继电器过温故障"},
{2, "A相绕组过温故障"},
{3, "B相绕组过温故障"},
{4, "C相绕组过温故障"},
{5, "逆变组件底板过温故障"},
{6, "输出滤波器过温故障"},
{7, "筒内空气过温故障"},
{8, "超速故障"},
{9, "紧急停机故障"},
{11, "协调通信丢失故障"},
{12, "协调故障停机"},
};
const FaultDesc kW0Branch2[] = {
{0, "CPLD自检故障标志"},
{1, "温度继电器过温故障"},
{2, "A相绕组过温故障"},
{3, "B相绕组过温故障"},
{4, "C相绕组过温故障"},
{5, "逆变组件底板过温故障"},
{6, "输出滤波器过温故障"},
// bit7 支路二为预留
{8, "超速故障"},
{9, "紧急停机故障"},
{11, "协调通信丢失故障"},
{12, "协调故障停机"},
};
//--------------------------------------------------------------------------
// 故障字2(Byte2|Byte3)
//--------------------------------------------------------------------------
const FaultDesc kW1[] = {
{0, "逆变A1相驱动1(上管)故障"},
{1, "逆变A1相驱动2(下管)故障"},
{4, "逆变B1相驱动3(上管)故障"},
{5, "逆变B1相驱动4(下管)故障"},
{8, "逆变C1相驱动5(上管)故障"},
{9, "逆变C1相驱动6(下管)故障"},
{12, "硬件总故障"},
{13, "驱动总故障"},
{14, "采样总故障"},
{15, "24V欠压故障"},
};
//--------------------------------------------------------------------------
// 故障字3(Byte4|Byte5)
//--------------------------------------------------------------------------
const FaultDesc kW2[] = {
{0, "A相软件过流故障"},
{1, "B相软件过流故障"},
{2, "C相软件过流故障"},
{3, "母线电压过压故障"},
{4, "母线电压欠压故障"},
{5, "输出缺相故障"},
{6, "预充电故障"},
{8, "A1相硬件过流故障"},
{9, "C1相硬件过流故障"},
{15, "母线过压硬件故障"},
};
//--------------------------------------------------------------------------
// 报警字(Byte6|Byte7)
//--------------------------------------------------------------------------
const FaultDesc kW3[] = {
{0, "直流母线1电压偏高报警"},
{1, "直流母线1电压偏低报警"},
{2, "A相绕组温度偏高报警"},
{3, "B相绕组温度偏高报警"},
{4, "C相绕组温度偏高报警"},
{5, "逆变组件底板温度偏高报警"},
{6, "输出滤波器温度偏高报警"},
{7, "筒内空气温度偏高报警"},
{8, "遥控CAN通信中断报警"},
{9, "485通信中断报警"},
{10, "协调CAN通信中断报警"},
{11, "参数设置失败报警"},
{12, "flash读取失败"},
{13, "直流母线2电压偏高报警"},
{14, "直流母线2电压偏低报警"},
{15, "协调光纤报警"},
};
const FaultDesc* tableOf(int branch, int wordIdx, size_t& count) {
switch (wordIdx) {
case 0:
if (branch == 2) { count = sizeof(kW0Branch2) / sizeof(kW0Branch2[0]); return kW0Branch2; }
count = sizeof(kW0Branch1) / sizeof(kW0Branch1[0]); return kW0Branch1;
case 1:
count = sizeof(kW1) / sizeof(kW1[0]); return kW1;
case 2:
count = sizeof(kW2) / sizeof(kW2[0]); return kW2;
case 3:
count = sizeof(kW3) / sizeof(kW3[0]); return kW3;
default:
count = 0; return nullptr;
}
}
// 16 位字(Intel:低字节在前)
uint16_t wordOf(const uint8_t* d, int byteIdx) {
return static_cast<uint16_t>(d[byteIdx] | (static_cast<uint16_t>(d[byteIdx + 1]) << 8));
}
// 重建某支路的故障/报警描述列表(word 更新后调用)
void rebuildBranchTexts(MotorBranchState& br) {
br.faults.clear();
br.alarms.clear();
for (int w = 0; w < 3; ++w) {
std::vector<std::string> tmp;
motorFaultTexts(br.branch, w, br.word[w], tmp);
br.faults.insert(br.faults.end(), tmp.begin(), tmp.end());
}
motorFaultTexts(br.branch, 3, br.word[3], br.alarms);
}
} // namespace
//--------------------------------------------------------------------------
// motorFaultTexts
int motorFaultTexts(int branch, int wordIdx, uint16_t bits,
std::vector<std::string>& out) {
out.clear();
if (branch < 1 || branch > 2) return 0;
size_t count = 0;
const FaultDesc* table = tableOf(branch, wordIdx, count);
if (!table) return 0;
for (size_t i = 0; i < count; ++i) {
if (bits & (1u << table[i].bit))
out.push_back(table[i].text);
}
return static_cast<int>(out.size());
}
//--------------------------------------------------------------------------
// buildMotorCmdData:指令帧数据域编码
// byte0 = enable(bit0) | reset(bit1)
// byte2/3 = speed s16(Intel 低字节在前)
void buildMotorCmdData(bool enable, bool reset, int16_t speed, uint8_t out[8]) {
std::memset(out, 0, 8);
out[0] = static_cast<uint8_t>((enable ? 0x01u : 0u) | (reset ? 0x02u : 0u));
uint16_t sp = static_cast<uint16_t>(speed);
out[2] = static_cast<uint8_t>(sp & 0xFFu);
out[3] = static_cast<uint8_t>((sp >> 8) & 0xFFu);
}
//--------------------------------------------------------------------------
// decodeMotorFrame:电机上行报文解析(部分更新合并进 st)
bool decodeMotorFrame(uint32_t canId, const uint8_t* data, int dlc, MotorState& st) {
if (!data || dlc < 8) return false;
switch (canId) {
case MOTOR_CAN_FAULT1:
case MOTOR_CAN_FAULT1_RED:
case MOTOR_CAN_FAULT2:
case MOTOR_CAN_FAULT2_RED: {
bool isBranch2 = (canId == MOTOR_CAN_FAULT2 || canId == MOTOR_CAN_FAULT2_RED);
MotorBranchState& br = isBranch2 ? st.branch2 : st.branch1;
br.branch = isBranch2 ? 2 : 1;
br.word[0] = wordOf(data, 0);
br.word[1] = wordOf(data, 2);
br.word[2] = wordOf(data, 4);
br.word[3] = wordOf(data, 6);
rebuildBranchTexts(br);
br.valid = true;
return true;
}
case MOTOR_CAN_STATE3:
case MOTOR_CAN_STATE3_RED: {
uint16_t sp = wordOf(data, 0);
st.feedbackSpeed = static_cast<int16_t>(sp);
st.busVoltage = wordOf(data, 2);
st.state3Valid = true;
return true;
}
case MOTOR_CAN_STATE4:
case MOTOR_CAN_STATE4_RED: {
st.tempA = static_cast<int8_t>(data[0]);
st.tempB = static_cast<int8_t>(data[1]);
st.tempC = static_cast<int8_t>(data[2]);
st.tempInv = static_cast<int8_t>(data[3]);
st.tempFilter = static_cast<int8_t>(data[4]);
st.tempAir = static_cast<int8_t>(data[5]);
st.state4Valid = true;
return true;
}
default:
return false;
}
}
} // namespace motor
+154
View File
@@ -0,0 +1,154 @@
#ifndef PMOTOR_CAN_H
#define PMOTOR_CAN_H
#include <cstdint>
#include <string>
#include <vector>
namespace motor {
//============================================================================
// 推进电机控制器 CAN 通信协议(docs/推进电机通信协议20260321.docx)
//
// 数据来源:pCanBridge 透传的 MOOS 消息
// 变量名 CAN_0x%08X = CAN ID(29 位扩展帧),m_sVal = 二进制数据域(8 字节),
// m_dfVal2 = 原始帧信息字节。本模块只负责按报文 ID 解析/编码数据域字段。
//
// 物理/链路层:CAN2.0B 扩展帧,250kbps(默认),数据域 8 字节,
// 多字节数值采用 Intel 编码(低字节在前,高字节在后)。
//
// 【报文清单】
// 下行(pMotor -> 电机控制器,周期 500ms):
// 0x18EF2010 主控指令帧(冗余 CAN 0x18EF2011)
// 0x18EF2110 主控限制帧(冗余 CAN 0x18EF2111,全预留,暂不发送)
// 上行(电机控制器 -> pMotor):
// 0x18EF1020 支路一故障报警状态帧1(200ms,冗余 CAN 0x18EF1021)
// 0x18EF1120 支路二故障报警状态帧2(200ms,冗余 CAN 0x18EF1121)
// 0x18EF1220 状态帧3:反馈转速/母线电压(100ms,冗余 0x18EF1221)
// 0x18EF1320 状态帧4:温度(100ms,冗余 0x18EF1321)
//
// 【指令帧 0x18EF2010】数据域 8 字节:
// byte0 bit0 = 使能命令(0 无效 1 有效,IGBT 开关;需先预充后使能)
// byte0 bit1 = 复位命令(0 无效 1 有效,仅故障状态下有效,故障支路复位)
// byte0 bit2 = 支路一通信状态(0 正常 1 中断)
// byte0 bit3 = 支路二通信状态(0 正常 1 中断)
// byte0 bit4~7 / byte1 / byte4~7 = 预留(发 0)
// byte2/3 = (转速模式)期望转速 s16(偏移 0,分辨率 1,Intel)
// 文档示例:启动并给定 1000 转速 -> 01 00 E8 03 00 00 00 00
// 停机 -> 00 00 00 00 00 00 00 00
// 复位 -> 02 00 00 00 00 00 00 00
//
// 【状态帧3 0x18EF1220】byte0/1 反馈转速 s16(Intel),byte2/3 母线电压 u16(Intel)
//
// 【状态帧4 0x18EF1320】byte0~5 依次为 A/B/C 相绕组、逆变组件底板、
// 输出滤波器、筒内空气温度,均为 s8(偏移 0,分辨率 1)
//
// 【故障帧 0x18EF1020/0x18EF1120】4 个 16 位字(byte0|1、byte2|3、byte4|5、byte6|7),
// 字内 bit 定义见 MotorCan.cpp 中描述表;前两帧仅支路一/支路二
// byte0|1 的 bit7 不同(支路一=筒内空气过温故障,支路二=预留)。
//============================================================================
// CAN ID 定义(29 位扩展帧)
enum MotorCanId : uint32_t {
MOTOR_CAN_CMD = 0x18EF2010u, // 主控指令帧(pMotor -> 电机)
MOTOR_CAN_CMD_RED = 0x18EF2011u, // 主控指令帧 冗余 CAN
MOTOR_CAN_LIMIT = 0x18EF2110u, // 主控限制帧(全预留,暂不使用)
MOTOR_CAN_LIMIT_RED = 0x18EF2111u, // 主控限制帧 冗余 CAN
MOTOR_CAN_FAULT1 = 0x18EF1020u, // 支路一故障报警状态帧1
MOTOR_CAN_FAULT1_RED = 0x18EF1021u, // 支路一 冗余 CAN
MOTOR_CAN_FAULT2 = 0x18EF1120u, // 支路二故障报警状态帧2
MOTOR_CAN_FAULT2_RED = 0x18EF1121u, // 支路二 冗余 CAN
MOTOR_CAN_STATE3 = 0x18EF1220u, // 状态帧3(转速/母线电压)
MOTOR_CAN_STATE3_RED = 0x18EF1221u, // 状态帧3 冗余 CAN
MOTOR_CAN_STATE4 = 0x18EF1320u, // 状态帧4(温度)
MOTOR_CAN_STATE4_RED = 0x18EF1321u, // 状态帧4 冗余 CAN
};
// 指令动作(网页 /api/motor_cmd -> 队列 -> Iterate 应用)
// 与协议指令帧字段一一对应,独立下发、无联动:
enum MotorAction {
MOTOR_ACT_ENABLE = 1, // 使能命令=1(Byte0 bit0,保持当前期望转速)
MOTOR_ACT_DISABLE = 2, // 使能命令=0(Byte0 bit0,协议"停机"示例:期望转速清 0)
MOTOR_ACT_SPEED = 3, // 期望转速(Byte2/3,speed 参数,使能状态保持不变)
MOTOR_ACT_RESET = 4, // 复位命令=1(Byte0 bit1,保持高 resetHold 秒,仅故障态有效)
};
// 网页下发的控制指令
struct MotorCmd {
int action = 0; // MotorAction
int16_t speed = 0; // MOTOR_ACT_SPEED / MOTOR_ACT_START 有效
};
// 单条电机上行消息的新鲜度 + 原始数据
struct MotorBranchState {
int branch = 0; // 1 / 2
uint16_t word[4] = {0, 0, 0, 0}; // 4 个 16 位字(故障1/故障2/故障3/报警)
std::vector<std::string> faults; // 当前置位的故障描述(word0~2)
std::vector<std::string> alarms; // 当前置位的报警描述(word3)
bool valid = false; // 是否收到过该支路故障帧
double lastRxTime = 0; // 最近收到时刻(MOOSTime,调用方设置)
};
// 电机最新合成状态(各帧部分更新合并)
struct MotorState {
// 状态帧3 0x18EF1220
int16_t feedbackSpeed = 0; // 反馈转速
uint16_t busVoltage = 0; // 母线电压
bool state3Valid = false;
double state3LastRx = 0;
// 状态帧4 0x18EF1320(s8,偏移 0)
int8_t tempA = 0, tempB = 0, tempC = 0; // A/B/C 相绕组温度
int8_t tempInv = 0, tempFilter = 0, tempAir = 0; // 逆变底板/滤波器/筒内空气
bool state4Valid = false;
double state4LastRx = 0;
// 故障/报警
MotorBranchState branch1;
MotorBranchState branch2;
// 计数
uint32_t rxCount = 0; // 命中电机报文的帧计数
};
// 是否为电机协议定义的 CAN ID(含冗余 CAN 变体)
inline bool isCanMotorId(uint32_t canId) {
switch (canId) {
case MOTOR_CAN_CMD:
case MOTOR_CAN_CMD_RED:
case MOTOR_CAN_LIMIT:
case MOTOR_CAN_LIMIT_RED:
case MOTOR_CAN_FAULT1:
case MOTOR_CAN_FAULT1_RED:
case MOTOR_CAN_FAULT2:
case MOTOR_CAN_FAULT2_RED:
case MOTOR_CAN_STATE3:
case MOTOR_CAN_STATE3_RED:
case MOTOR_CAN_STATE4:
case MOTOR_CAN_STATE4_RED:
return true;
default:
return false;
}
}
// 故障/报警字索引(帧内):0=byte0|1 1=byte2|3 2=byte4|5 3=byte6|7(报警)
extern const char* kMotorWordName[4];
// 按支路(1/2)与字索引查故障/报警描述表,把 bits 中置位的描述填入 out;
// 返回描述条数。wordIdx 非法或无置位返回 0。
int motorFaultTexts(int branch, int wordIdx, uint16_t bits,
std::vector<std::string>& out);
// 编码指令帧数据域(8 字节,经 MOOS CAN_TX_0x%08X 下发 pCanBridge 发送):
// [0] = enable(bit0) | reset(bit1)(支路通信状态位恒 0)
// [1] = 0 [2]/[3] = speed s16 Intel(停用时调用方传 0) [4..7] = 0
void buildMotorCmdData(bool enable, bool reset, int16_t speed, uint8_t out[8]);
// 解析一帧 CAN 数据域;命中电机上行报文返回 true 并部分更新 st:
// 故障帧 -> 对应支路 word[4] + 故障/报警描述
// 状态帧3 -> feedbackSpeed / busVoltage
// 状态帧4 -> 6 路温度
// 非电机报文或 data/dlc 非法返回 false。lastRxTime 由调用方设置。
bool decodeMotorFrame(uint32_t canId, const uint8_t* data, int dlc, MotorState& st);
} // namespace motor
#endif // PMOTOR_CAN_H
+144
View File
@@ -0,0 +1,144 @@
#include "WebServer.h"
#include "pages/index.h"
#include <iostream>
namespace motor {
//---------------------------------------------------------
// 事件处理: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;
}
// /api/* 接口:交给宿主程序处理,返回 JSON
std::string path(hm->uri.buf, hm->uri.len);
if (path.rfind("/api/", 0) == 0) {
std::string body;
if (m_apiHandler) {
std::string query(hm->query.buf, hm->query.len);
body = m_apiHandler(path, query);
}
if (!body.empty()) {
mg_http_reply(c, 200, "Content-Type: application/json\r\n",
"%.*s", (int)body.size(), body.c_str());
} else {
mg_http_reply(c, 404, "Content-Type: text/plain\r\n", "Not Found\n");
}
return;
}
// 电机控制页面
if (path == "/" || path == "/index.html") {
mg_http_reply(c, 200, "Content-Type: text/html; charset=utf-8\r\n",
"%.*s", (int)INDEX_HTML.size(), INDEX_HTML.c_str());
return;
}
mg_http_reply(c, 404, "Content-Type: text/plain\r\n", "Not Found\n");
} else if (ev == MG_EV_WS_OPEN) {
// Web 线程内:先构建欢迎快照,再登记连接
std::string welcome;
if (m_onOpen) welcome = m_onOpen();
m_wsConnections.push_back(c);
if (!welcome.empty()) {
mg_ws_send(c, welcome.c_str(), welcome.size(), WEBSOCKET_OP_TEXT);
}
} else if (ev == MG_EV_CLOSE || ev == MG_EV_ERROR) {
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) {
// 忽略客户端消息(控制走 /api/*)
}
}
WebServer::WebServer() {
mg_mgr_init(&m_mgr);
}
WebServer::~WebServer() {
stop();
mg_mgr_free(&m_mgr);
}
void WebServer::setOnOpen(std::function<std::string()> handler) {
m_onOpen = std::move(handler);
}
void WebServer::setApiHandler(std::function<std::string(const std::string&, const std::string&)> handler) {
m_apiHandler = std::move(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 << "pMotor 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();
// Web 线程已退出,此处触碰连接列表安全
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);
// 仅在本线程(poll 间隙)发送,避免跨线程操作 mongoose 连接
flushPending();
}
return true;
}
void WebServer::flushPending() {
std::string text;
{
std::lock_guard<std::mutex> lock(m_pendingMutex);
text = std::move(m_pending);
m_pending.clear();
}
if (text.empty()) return;
for (auto c : m_wsConnections) {
mg_ws_send(c, text.c_str(), text.size(), WEBSOCKET_OP_TEXT);
}
}
void WebServer::broadcast(const std::string& text) {
// 只投递到队列,实际发送在 Web 线程 flushPending() 中完成
std::lock_guard<std::mutex> lock(m_pendingMutex);
m_pending = text;
}
} // namespace motor
+66
View File
@@ -0,0 +1,66 @@
#ifndef PMOTOR_WEB_SERVER_H
#define PMOTOR_WEB_SERVER_H
#define UNIX
#include "mongoose.h"
#include <string>
#include <functional>
#include <vector>
#include <mutex>
#include <atomic>
#include <thread>
namespace motor {
//============================================================================
// WebServer:HTTP + WebSocket 控制/监控服务器(基于 mongoose)。
//
// 路由:
// / -> 内嵌电机控制页面
// /ws -> WebSocket,推送 JSON 快照(由 Motor 周期调用 broadcast)
// /api/* -> 控制指令接口(由 Motor 处理,返回 JSON)
// 采用与仓库内 pCCU/WebServer、pPowerMangerHost/WebServer 一致的回调模式。
//============================================================================
class WebServer {
public:
WebServer();
~WebServer();
bool start(int port);
void stop();
bool isRunning() const { return m_running; }
// 向所有 WebSocket 客户端广播文本
// 线程安全:mongoose 非线程安全,本接口只把文本放入待发队列,
// 由 Web 线程在 mg_mgr_poll 返回后统一发送(禁止跨线程直接 mg_ws_send)
void broadcast(const std::string& text);
// 新客户端连接时调用,返回初始快照
void setOnOpen(std::function<std::string()> handler);
// 处理 /api/* 请求,返回 JSON 响应体(空串表示 404)
void setApiHandler(std::function<std::string(const std::string& uri, const std::string& query)> handler);
private:
static bool serverThreadCB(void* pParam);
bool serverThreadFunc();
void handleEvent(struct mg_connection* c, int ev, void* ev_data);
// Web 线程内:把待发队列中的快照推给所有 WS 客户端(仅本线程触碰连接列表)
void flushPending();
struct mg_mgr m_mgr;
int m_port = 0;
std::atomic<bool> m_running{false};
std::thread m_thread;
std::mutex m_pendingMutex; // 保护 m_pending
std::string m_pending; // 最新待广播快照(新快照覆盖旧快照)
std::vector<mg_connection*> m_wsConnections; // 仅 Web 线程访问
std::function<std::string()> m_onOpen;
std::function<std::string(const std::string&, const std::string&)> m_apiHandler;
};
} // namespace motor
#endif // PMOTOR_WEB_SERVER_H
+271
View File
@@ -0,0 +1,271 @@
#ifndef PMOTOR_PAGE_INDEX_H
#define PMOTOR_PAGE_INDEX_H
#include <string>
//============================================================================
// pMotor 推进电机控制页面(纯前端,内嵌 HTML)。
//
// - 通过 WebSocket /ws 接收服务器推送的 JSON 快照(每秒)
// - 控制:按协议指令帧字段独立下发(无联动按钮)
// 使能命令(Byte0 bit0) / 复位命令(Byte0 bit1) / 期望转速(Byte2/3)
// (fetch /api/motor_cmd -> pMotor 入队 -> MOOS CAN_TX -> pCanBridge -> CAN1)
// - 展示:反馈转速/母线电压、温度、支路一/支路二故障与报警(按位解析中文)
// - 协议定义:docs/推进电机通信协议20260321.docx
//============================================================================
namespace motor {
const std::string INDEX_HTML = R"HTML(<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>pMotor 推进电机控制</title>
<style>
:root{--bg:#0f1420;--card:#1a2130;--line:#2a3447;--fg:#e6edf3;--dim:#8b98ab;--ok:#2ea043;--warn:#d29922;--bad:#f85149;}
*{margin:0;padding:0;box-sizing:border-box;}
body{background:var(--bg);color:var(--fg);font-family:'Segoe UI',system-ui,sans-serif;font-size:13px;padding:16px;}
h1{font-size:18px;margin-bottom:12px;display:flex;align-items:center;gap:10px;}
h2{font-size:13px;color:var(--dim);text-transform:uppercase;letter-spacing:.5px;margin-bottom:10px;border-bottom:1px solid var(--line);padding-bottom:6px;}
.badge{font-size:11px;padding:2px 8px;border-radius:10px;background:var(--card);border:1px solid var(--line);}
.badge.ok{color:var(--ok);border-color:var(--ok);}
.badge.warn{color:var(--warn);border-color:var(--warn);}
.badge.err{color:var(--bad);border-color:var(--bad);}
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:12px;}
.card{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:12px;}
.row{display:flex;justify-content:space-between;padding:3px 0;border-bottom:1px dashed #222c3d;}
.row:last-child{border-bottom:none;}
.row .k{color:var(--dim);}
.row .v{font-family:Consolas,monospace;color:var(--fg);text-align:right;}
.sub{font-size:11px;color:#79c0ff;margin:10px 0 2px;border-bottom:1px solid #22304a;padding-bottom:2px;}
.sub:first-child{margin-top:0;}
.btn{padding:6px 14px;margin:0 8px 8px 0;border:1px solid var(--line);border-radius:6px;background:#222b3d;color:var(--fg);font-size:12px;cursor:pointer;}
.btn:hover{border-color:#3b4a63;}
.btn.on{background:#1f6feb;border-color:#1f6feb;color:#fff;font-weight:600;}
.btn.off{background:#3d2226;border-color:#f85149;color:#f85149;}
.btn.warn{background:#3a2e12;border-color:var(--warn);color:var(--warn);}
.btn:disabled{opacity:.4;cursor:not-allowed;}
input[type=number]{width:110px;background:#0f1420;border:1px solid var(--line);border-radius:6px;color:var(--fg);font-family:Consolas,monospace;padding:5px 8px;font-size:13px;}
input[type=number]:focus{outline:none;border-color:#1f6feb;}
.fc-bad{color:#f85149;font-weight:700;background:rgba(248,81,73,.12);padding:1px 6px;border-radius:4px;display:inline-block;margin:2px 4px 2px 0;}
.fc-warn{color:#d29922;font-weight:600;background:rgba(210,153,34,.10);padding:1px 6px;border-radius:4px;display:inline-block;margin:2px 4px 2px 0;}
.fc-ok{color:#3fb950;font-weight:600;}
#status{color:var(--dim);font-size:12px;}
.state-big{font-size:22px;font-family:Consolas,monospace;}
</style>
</head>
<body>
<h1>pMotor 推进电机控制 <span id="status" class="badge">连接中...</span></h1>
<div class="grid" id="pages">加载中...</div>
<script>
const $ = id => document.getElementById(id);
const pagesEl = $('pages');
const statusEl = $('status');
function row(k, v){ return '<div class="row"><span class="k">'+k+'</span><span class="v">'+v+'</span></div>'; }
function sub(t){ return '<div class="sub">'+t+'</div>'; }
function hex(v){ return '0x' + (v & 0xFF).toString(16).toUpperCase().padStart(2,'0'); }
function hex16(v){ return '0x' + (v & 0xFFFF).toString(16).toUpperCase().padStart(4,'0'); }
function card(title, html){ return '<div class="card"><h2>'+title+'</h2>'+html+'</div>'; }
function ageText(a){
if(typeof a !== 'number' || a < 0) return '从未收到';
if(a < 90) return a.toFixed(1)+' 秒前';
if(a < 3600) return Math.floor(a/60)+' 分 '+Math.floor(a%60)+' 秒前';
return Math.floor(a/3600)+' 时 '+Math.floor((a%3600)/60)+' 分前';
}
function ageCell(d){
if(!d || !d.valid) return '<span style="color:var(--dim)">从未收到</span>';
const online = d.age < 3;
return (online ? '<span class="fc-ok">' : '<span class="fc-bad">')
+ ageText(d.age) + (online ? ' (在线)' : ' (超时)') + '</span>';
}
//------------------ 控制指令(/api/motor_cmd) ------------------
// 链路: 网页 fetch -> pMotor 入队 -> Iterate 周期编码 0x18EF2010
// -> MOOS CAN_TX_0x18EF2010 -> pCanBridge -> CAN1 -> 电机控制器
function sendCmd(action, speed, confirmMsg){
if(confirmMsg && !confirm(confirmMsg)) return;
let url = '/api/motor_cmd?action='+action;
if(speed !== undefined && speed !== null) url += '&speed='+speed;
fetch(url)
.then(function(r){ return r.json(); })
.then(function(j){
if(j.ok) alert('指令已下发: '+j.action+(j.action==='speed' ? ' '+(j.speed||0)+' rpm' : ''));
else alert('下发失败: '+(j.error||'未知错误'));
})
.catch(function(){ alert('网络错误,指令未下发'); });
}
function motorEnable(){
sendCmd('enable', null, '确认启动推进电机(使能=1,IGBT 开管)?\n电机将按当前期望转速运行。');
}
function motorStop(){
sendCmd('disable', null, '确认停机(使能=0,期望转速清零)?');
}
function motorReset(){
sendCmd('reset', null, '确认下发故障复位指令?\n复位命令仅在故障状态下有效,故障支路将执行复位。');
}
function readSpeed(){
const el = $('speedInput');
const v = parseInt(el.value, 10);
if(isNaN(v)){ alert('请输入有效整数转速'); return null; }
if(v < -32767 || v > 32767){ alert('转速范围 -32767 ~ 32767'); return null; }
return v;
}
function motorSetSpeed(){
const v = readSpeed(); if(v === null) return;
sendCmd('speed', v, '确认设定期望转速 '+v+' rpm?\n(对应指令帧 Byte2/3,使能状态保持不变)');
}
//------------------ 控制卡片 ------------------
function cmdCard(s){
const c = s || {};
const cmd = c.cmd || {};
const en = cmd.enable;
let h = '';
h += row('使能状态', en
? '<span class="fc-ok">已使能(运行)</span>'
: '<span style="color:var(--dim)">未使能(停机)</span>');
h += row('期望转速', (cmd.speed||0) + ' rpm');
h += row('复位命令', cmd.resetActive ? '<span class="fc-warn">生效中(保持数秒)</span>' : '无效');
if(cmd.lastCmdValid){
const names = {enable:'使能命令=1', disable:'使能命令=0(停机)', reset:'复位命令=1', speed:'期望转速'};
h += row('最近指令', (names[cmd.lastCmdAction]||cmd.lastCmdAction)
+ (cmd.lastCmdAction==='speed' ? ' ('+(cmd.lastCmdSpeed||0)+' rpm)' : '')
+ ' · ' + ageText(cmd.lastCmdAge));
} else {
h += row('最近指令', '<span style="color:var(--dim)">无</span>');
}
h += '<div class="sub">使能命令 (Byte0 bit0)</div>' +
'<div>' +
'<button class="btn on" onclick="motorEnable()">使能 (1)</button>' +
'<button class="btn off" onclick="motorStop()">去使能 (0)</button></div>' +
'<div class="sub">复位命令 (Byte0 bit1,仅故障状态下有效)</div>' +
'<div>' +
'<button class="btn warn" onclick="motorReset()">复位</button></div>' +
'<div class="sub">期望转速 (Byte2/3,s16,单位 rpm)</div>' +
'<div>' +
'<input type="number" id="speedInput" value="1000" min="-32767" max="32767" step="100" title="期望转速 (rpm)">' +
'<span style="color:var(--dim);margin-left:6px;">rpm</span> ' +
'<button class="btn" onclick="motorSetSpeed()">设定</button></div>' +
'<div class="sub" style="margin-top:8px;">指令帧 0x18EF2010 周期 ' + ((c.link&&c.link.periodMs)||500) +
'ms 持续发送(协议要求);停机=使能0且转速清0(协议示例 00 00 00 00 00 00 00 00)</div>';
return card('电机指令 (下行 0x18EF2010)', h);
}
//------------------ 状态帧3:转速 / 母线电压 ------------------
function state3Card(d){
if(!d) return '';
let h = '';
h += row('反馈转速', '<span class="state-big">'+(d.valid?(d.speed||0):'—')+'</span> rpm');
h += row('母线电压', (d.valid?(d.voltage||0):'—') + (d.valid?' V':''));
h += row('最后更新', ageCell(d));
h += sub('来源 0x18EF1220(100ms,冗余 0x18EF1221)');
return card('运行状态', h);
}
//------------------ 状态帧4:温度 ------------------
function state4Card(d){
if(!d) return '';
const v = d.valid;
let h = '';
const temps = [
['A相绕组温度', d.tA], ['B相绕组温度', d.tB], ['C相绕组温度', d.tC],
['逆变组件底板温度', d.tInv], ['输出滤波器温度', d.tFilter], ['筒内空气温度', d.tAir],
];
for(const t of temps){
h += row(t[0], v ? (t[1]|0) + ' ℃' : '—');
}
h += row('最后更新', ageCell(d));
h += sub('来源 0x18EF1320(100ms,冗余 0x18EF1321,Byte0~5 各 1 字节 s8)');
return card('温度', h);
}
//------------------ 支路故障/报警卡片 ------------------
function branchCard(title, d){
if(!d) return '';
const words = d.words || [];
let h = '';
const faults = d.faults || [];
const alarms = d.alarms || [];
if(faults.length){
h += row('故障(' + faults.length + '条)', faults.map(function(f){ return '<span class="fc-bad">'+f+'</span>'; }).join(''));
} else {
h += row('故障', d.valid ? '<span class="fc-ok">无</span>' : '—');
}
if(alarms.length){
h += row('报警(' + alarms.length + '条)', alarms.map(function(a){ return '<span class="fc-warn">'+a+'</span>'; }).join(''));
} else {
h += row('报警', d.valid ? '<span class="fc-ok">无</span>' : '—');
}
h += row('故障字原始值', words.map(hex16).join(' '));
h += row('最后更新', ageCell(d));
return card(title, h);
}
//------------------ 通信链路卡片 ------------------
function linkCard(d){
const l = (d && d.link) || {};
let h = '';
h += row('下行通道 (CAN1)', l.channel || 'CAN1');
h += row('冗余通道', l.redChannel ? l.redChannel : '<span style="color:var(--dim)">未配置</span>');
h += row('指令帧周期', (l.periodMs||0) + ' ms');
h += row('指令帧累计', (l.txCount||0) + ' 成功 / ' + (l.txErr||0) + ' 失败');
h += row('最近下发', ageCell({valid:!!l.lastTxValid, age:l.lastTxAge}));
h += row('电机报文接收', (l.rxCount||0) + ' 帧');
h += sub('链路: 电机 <-> CAN1 <-> CANET <-> pCanBridge <-> MOOS <-> pMotor');
return card('通信链路 (pCanBridge)', h);
}
//------------------ 协议参考卡片 ------------------
function protoCard(){
let h = '';
h += row('0x18EF2010', '主控指令帧(下行 500ms,冗余 0x18EF2011)');
h += row('0x18EF2110', '主控限制帧(下行,全预留,暂不使用)');
h += row('0x18EF1020', '支路一故障报警状态帧1(上行 200ms,冗余 0x18EF1021)');
h += row('0x18EF1120', '支路二故障报警状态帧2(上行 200ms,冗余 0x18EF1121)');
h += row('0x18EF1220', '状态帧3:反馈转速/母线电压(上行 100ms,冗余 0x18EF1221)');
h += row('0x18EF1320', '状态帧4:温度(上行 100ms,冗余 0x18EF1321)');
h += row('帧格式', 'CAN2.0B 扩展帧 29 位 ID,数据域 8 字节,Intel 字节序');
h += row('波特率', '250 kbps(默认,按客户可定制 125~1000)');
return card('报文定义 (推进电机通信协议 20260321)', h);
}
function render(snap){
let html = '';
html += cmdCard(snap);
html += state3Card(snap.state3);
html += state4Card(snap.state4);
html += branchCard('支路一故障/报警 (0x18EF1020)', snap.fault1);
html += branchCard('支路二故障/报警 (0x18EF1120)', snap.fault2);
html += linkCard(snap);
html += protoCard();
pagesEl.innerHTML = html;
// 保留转速输入值(render 会重建 DOM)
const prev = window.__lastSpeed;
if(prev !== undefined){ const el = $('speedInput'); if(el) el.value = prev; }
}
function connect(){
const proto = location.protocol==='https:'?'wss':'ws';
const ws = new WebSocket(proto+'://'+location.host+'/ws');
ws.onopen = ()=>{ statusEl.textContent='已连接'; statusEl.className='badge ok'; };
ws.onclose = ()=>{ statusEl.textContent='已断开,重连中...'; statusEl.className='badge err'; setTimeout(connect, 2000); };
ws.onerror = ()=>{ ws.close(); };
ws.onmessage = (ev)=>{
try{
const snap = JSON.parse(ev.data);
const el = $('speedInput');
if(el && el === document.activeElement) window.__lastSpeed = el.value;
render(snap);
}catch(e){}
};
}
connect();
</script>
</body>
</html>
)HTML";
} // namespace motor
#endif // PMOTOR_PAGE_INDEX_H
+1 -1
View File
@@ -168,7 +168,7 @@ class PowerManger : public AppCastingMOOSApp
double GetPublicLastIterateTime() { return GetLastIterateTime(); }
int GetPublicIterateCount() { return GetIterateCount(); }
string GetServerHost() { return m_sServerHost; }
int GetServerPort() { return m_lServerPort; }
int GetServerPort() { return m_lServerPort; }
public:
//故障注入
void injectFault(unsigned type,unsigned int faultCode);
+20
View File
@@ -161,3 +161,23 @@ target_link_libraries(pccuTest
pthread
dl
)
################################################pmotorTest(pMotor 单元测试)####
# pMotor 推进电机 CAN 协议编解码单元测试(独立可执行,无 gtest 依赖)
set(MOTOR_DIR ${CMAKE_SOURCE_DIR}/src/pMotor)
add_executable(pmotorTest
pmotor/pmotorTest.cpp
${MOTOR_DIR}/protocol/MotorCan.cpp
)
target_include_directories(pmotorTest PRIVATE
${MOTOR_DIR}
)
set_target_properties(pmotorTest PROPERTIES
CXX_STANDARD 11
CXX_STANDARD_REQUIRED ON
)
target_link_libraries(pmotorTest
m
pthread
)
+255
View File
@@ -0,0 +1,255 @@
//============================================================================
// pmotorTest:pMotor 推进电机 CAN 协议编解码单元测试
// 无第三方测试库,独立可执行。参照 test/pccu/pccuTest.cpp 的轻量风格。
//
// 协议依据:docs/推进电机通信协议20260321.docx
//============================================================================
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
#include "../../src/pMotor/protocol/MotorCan.h"
using namespace motor;
static int g_fail = 0;
static int g_pass = 0;
#define CHECK(cond) do { \
if (cond) { ++g_pass; } \
else { ++g_fail; std::printf("FAIL %s:%d %s\n", __FILE__, __LINE__, #cond); } \
} while (0)
#define CHECK_STREQ(a, b) do { \
if (std::string(a) == std::string(b)) { ++g_pass; } \
else { ++g_fail; std::printf("FAIL %s:%d \"%s\" != \"%s\"\n", __FILE__, __LINE__, std::string(a).c_str(), std::string(b).c_str()); } \
} while (0)
//--------------------------------------------------------------------------
// 1. 指令帧编码(对照文档示例)
static void testCmdEncode() {
uint8_t d[8];
// 文档示例:启动并给定 1000 转速 -> 01 00 E8 03 00 00 00 00
buildMotorCmdData(true, false, 1000, d);
uint8_t expect1[] = {0x01, 0x00, 0xE8, 0x03, 0x00, 0x00, 0x00, 0x00};
CHECK(std::memcmp(d, expect1, 8) == 0);
// 文档示例:停机 -> 00 00 00 00 00 00 00 00
buildMotorCmdData(false, false, 0, d);
uint8_t expect0[] = {0, 0, 0, 0, 0, 0, 0, 0};
CHECK(std::memcmp(d, expect0, 8) == 0);
// 文档示例:复位 -> 02 00 00 00 00 00 00 00
buildMotorCmdData(false, true, 0, d);
uint8_t expect2[] = {0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
CHECK(std::memcmp(d, expect2, 8) == 0);
// 使能+复位 同时有效 -> bit0+bit1 = 0x03
buildMotorCmdData(true, true, 0, d);
CHECK(d[0] == 0x03);
// 负转速 s16 Intel:-100 = 0xFF9C -> byte2=0x9C byte3=0xFF
buildMotorCmdData(true, false, -100, d);
CHECK(d[0] == 0x01);
CHECK(d[2] == 0x9C && d[3] == 0xFF);
// 边界转速:32767 / -32768
buildMotorCmdData(true, false, 32767, d);
CHECK(d[2] == 0xFF && d[3] == 0x7F);
buildMotorCmdData(true, false, -32768, d);
CHECK(d[2] == 0x00 && d[3] == 0x80);
// 支路通信状态位(bit2/bit3)恒 0(pMotor 不反馈通信状态)
buildMotorCmdData(true, false, 1, d);
CHECK((d[0] & 0x0C) == 0);
}
//--------------------------------------------------------------------------
// 2. CAN ID 识别
static void testCanId() {
// 12 条电机 ID 全部命中
const uint32_t ids[] = {
MOTOR_CAN_CMD, MOTOR_CAN_CMD_RED,
MOTOR_CAN_LIMIT, MOTOR_CAN_LIMIT_RED,
MOTOR_CAN_FAULT1, MOTOR_CAN_FAULT1_RED,
MOTOR_CAN_FAULT2, MOTOR_CAN_FAULT2_RED,
MOTOR_CAN_STATE3, MOTOR_CAN_STATE3_RED,
MOTOR_CAN_STATE4, MOTOR_CAN_STATE4_RED,
};
for (size_t i = 0; i < sizeof(ids) / sizeof(ids[0]); ++i)
CHECK(isCanMotorId(ids[i]));
// 冗余 CAN 与主 CAN 仅末字节差 1
CHECK(MOTOR_CAN_CMD_RED == MOTOR_CAN_CMD + 1);
CHECK(MOTOR_CAN_FAULT1_RED == MOTOR_CAN_FAULT1 + 1);
CHECK(MOTOR_CAN_FAULT2_RED == MOTOR_CAN_FAULT2 + 1);
CHECK(MOTOR_CAN_STATE3_RED == MOTOR_CAN_STATE3 + 1);
CHECK(MOTOR_CAN_STATE4_RED == MOTOR_CAN_STATE4 + 1);
// 其他协议报文不命中(BCU 电池 / 配电)
CHECK(!isCanMotorId(0x10010000)); // BCU 概要
CHECK(!isCanMotorId(0x103681FF)); // BCU 继电器控制
CHECK(!isCanMotorId(0x1401A501)); // 配电控制指令
CHECK(!isCanMotorId(0x18EF2012)); // 电机协议未定义的 ID
}
//--------------------------------------------------------------------------
// 3. 状态帧3 解码(0x18EF1220:反馈转速/母线电压)
static void testState3Decode() {
MotorState st;
uint8_t d[8] = {0xE8, 0x03, 0xF4, 0x01, 0, 0, 0, 0};
CHECK(decodeMotorFrame(MOTOR_CAN_STATE3, d, 8, st));
CHECK(st.state3Valid);
CHECK(st.feedbackSpeed == 1000); // 0x03E8 Intel
CHECK(st.busVoltage == 500); // 0x01F4 Intel
// 负转速
uint8_t d2[8] = {0x9C, 0xFF, 0, 0, 0, 0, 0, 0};
CHECK(decodeMotorFrame(MOTOR_CAN_STATE3, d2, 8, st));
CHECK(st.feedbackSpeed == -100);
// 冗余 CAN 同样可解码
MotorState st2;
CHECK(decodeMotorFrame(MOTOR_CAN_STATE3_RED, d, 8, st2));
CHECK(st2.feedbackSpeed == 1000 && st2.busVoltage == 500);
// 非法 dlc
MotorState st3;
CHECK(!decodeMotorFrame(MOTOR_CAN_STATE3, d, 4, st3));
CHECK(!decodeMotorFrame(MOTOR_CAN_STATE3, nullptr, 8, st3));
}
//--------------------------------------------------------------------------
// 4. 状态帧4 解码(0x18EF1320:温度)
static void testState4Decode() {
MotorState st;
uint8_t d[8] = {26, 44, 62, 79, 90, 107, 0, 0};
CHECK(decodeMotorFrame(MOTOR_CAN_STATE4, d, 8, st));
CHECK(st.state4Valid);
CHECK(st.tempA == 26 && st.tempB == 44 && st.tempC == 62);
CHECK(st.tempInv == 79 && st.tempFilter == 90 && st.tempAir == 107);
// 冗余 CAN
MotorState st2;
CHECK(decodeMotorFrame(MOTOR_CAN_STATE4_RED, d, 8, st2));
CHECK(st2.tempA == 26);
}
//--------------------------------------------------------------------------
// 5. 故障帧解码(0x18EF1020 支路一 / 0x18EF1120 支路二)
static void testFaultDecode() {
MotorState st;
// 支路一:word0 置 bit8(超速) + bit9(紧急停机) -> byte0|byte1<<8 = 0x0300;
// word3 置 bit8(遥控CAN通信中断报警) -> 0x0100
uint8_t d1[8] = {0x00, 0x03, 0, 0, 0, 0, 0x00, 0x01};
CHECK(decodeMotorFrame(MOTOR_CAN_FAULT1, d1, 8, st));
CHECK(st.branch1.valid);
CHECK(st.branch1.word[0] == 0x0300);
CHECK(st.branch1.word[3] == 0x0100);
CHECK(st.branch1.faults.size() == 2);
CHECK_STREQ(st.branch1.faults[0], "超速故障");
CHECK_STREQ(st.branch1.faults[1], "紧急停机故障");
CHECK(st.branch1.alarms.size() == 1);
CHECK_STREQ(st.branch1.alarms[0], "遥控CAN通信中断报警");
// 支路一 word0 bit7 = 筒内空气过温故障
uint8_t d2[8] = {0x80, 0x00, 0, 0, 0, 0, 0, 0};
CHECK(decodeMotorFrame(MOTOR_CAN_FAULT1, d2, 8, st));
CHECK(st.branch1.faults.size() == 1);
CHECK_STREQ(st.branch1.faults[0], "筒内空气过温故障");
// 支路二:同 bit7 为预留,不应产生故障描述
uint8_t d3[8] = {0x80, 0x00, 0, 0, 0, 0, 0, 0};
CHECK(decodeMotorFrame(MOTOR_CAN_FAULT2, d3, 8, st));
CHECK(st.branch2.valid);
CHECK(st.branch2.faults.empty());
CHECK(st.branch2.alarms.empty());
// 支路二 word1:bit0(逆变A1相驱动1上管) + word2 bit3(母线电压过压)
uint8_t d4[8] = {0, 0, 0x01, 0x00, 0x08, 0x00, 0, 0};
CHECK(decodeMotorFrame(MOTOR_CAN_FAULT2, d4, 8, st));
CHECK(st.branch2.word[1] == 0x0001);
CHECK(st.branch2.word[2] == 0x0008);
CHECK(st.branch2.faults.size() == 2);
CHECK_STREQ(st.branch2.faults[0], "逆变A1相驱动1(上管)故障");
CHECK_STREQ(st.branch2.faults[1], "母线电压过压故障");
// 冗余 CAN 同样可解码且归入对应支路
MotorState st2;
CHECK(decodeMotorFrame(MOTOR_CAN_FAULT1_RED, d1, 8, st2));
CHECK(st2.branch1.valid && !st2.branch2.valid);
CHECK(decodeMotorFrame(MOTOR_CAN_FAULT2_RED, d4, 8, st2));
CHECK(st2.branch2.valid && st2.branch2.faults.size() == 2);
}
//--------------------------------------------------------------------------
// 6. 故障描述查表
static void testFaultTable() {
std::vector<std::string> out;
// word1 bit12/13/14/15
CHECK(motorFaultTexts(1, 1, (1u << 12) | (1u << 13) | (1u << 14) | (1u << 15), out) == 4);
CHECK_STREQ(out[0], "硬件总故障");
CHECK_STREQ(out[1], "驱动总故障");
CHECK_STREQ(out[2], "采样总故障");
CHECK_STREQ(out[3], "24V欠压故障");
// word3 全位 16 条
CHECK(motorFaultTexts(1, 3, 0xFFFF, out) == 16);
CHECK_STREQ(out[0], "直流母线1电压偏高报警");
CHECK_STREQ(out[15], "协调光纤报警");
// 无置位
CHECK(motorFaultTexts(1, 0, 0, out) == 0);
// 非法参数
CHECK(motorFaultTexts(0, 0, 1, out) == 0);
CHECK(motorFaultTexts(3, 0, 1, out) == 0);
CHECK(motorFaultTexts(1, 9, 1, out) == 0);
}
//--------------------------------------------------------------------------
// 7. 部分更新合并语义(故障帧重复上报,故障清除后描述随之清除)
static void testPartialUpdate() {
MotorState st;
uint8_t d1[8] = {0x01, 0x00, 0, 0, 0, 0, 0, 0}; // 支路一 CPLD自检故障 (bit0)
CHECK(decodeMotorFrame(MOTOR_CAN_FAULT1, d1, 8, st));
CHECK(st.branch1.faults.size() == 1);
CHECK_STREQ(st.branch1.faults[0], "CPLD自检故障标志");
// 复位后故障字清零 -> 描述清空
uint8_t d2[8] = {0, 0, 0, 0, 0, 0, 0, 0};
CHECK(decodeMotorFrame(MOTOR_CAN_FAULT1, d2, 8, st));
CHECK(st.branch1.faults.empty());
// 状态帧不影响支路有效标志
uint8_t d3[8] = {1, 0, 0, 0, 0, 0, 0, 0};
CHECK(decodeMotorFrame(MOTOR_CAN_STATE3, d3, 8, st));
CHECK(st.feedbackSpeed == 1);
CHECK(st.branch1.valid); // 仍为 true(部分更新语义)
}
int main() {
testCmdEncode();
testCanId();
testState3Decode();
testState4Decode();
testFaultDecode();
testFaultTable();
testPartialUpdate();
std::printf("\n==== pmotorTest: %d passed, %d failed ====\n", g_pass, g_fail);
return g_fail ? 1 : 0;
}