diff --git a/missions/h100.moos b/missions/h100.moos index 11ed1ad..ecf3f72 100644 --- a/missions/h100.moos +++ b/missions/h100.moos @@ -126,7 +126,7 @@ ProcessConfig = pELoad //======== 电子负载(IT6000C)以太网地址 ======== // 仪器 LAN 配置的 IP 与 Raw Socket 端口号(出厂默认 30000) - eload_ip = 192.168.0.10 + eload_ip = 192.168.200.100 eload_port = 30000 //======== 功率控制 ======== @@ -141,11 +141,18 @@ ProcessConfig = pELoad slew_limit_wps = 0 func_priority = cc + // CV 优先时的目标电压(V),func_priority=cv 时生效(恒压吸收) + volt_set = 0 + //======== 数据存储 ======== dbpath = /root/work/h100/data/pELoad.db log_keep_hours = 72 meas_log_period_ms = 100 + // 测量命令来源:fetch=FETCh(读设备表头缓存,不触发测量,响应快,推荐); + // meas=MEAS(每次触发设备重新测量,响应慢,易超时) + meas_source = fetch + //======== 网页(避开 8080/8090/18080/18081)======== web_port = 18082 web_enable = true diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 05fc88c..69173fb 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -1,16 +1,17 @@ #!/bin/bash #======================================================================= # FILE: scripts/deploy.sh -# DESC: 部署 mission + 5 个 systemd 服务(moosdb / pPowerManger / -# pPowerMangerHost / pCCU / pCanBridge)到目标板卡 (RK3588 / -# Ubuntu 22.04)。只安装服务 + daemon-reload,默认不启动、不自启。 +# DESC: 部署 mission + 7 个 systemd 服务(moosdb / pPowerManger / +# pPowerMangerHost / pCCU / pCanBridge / pMotor / pELoad)到目标 +# 板卡 (RK3588 / Ubuntu 22.04)。只安装服务 + daemon-reload, +# 默认不启动、不自启。 # # 注意:二进制由 ./scripts/build-board.sh 在板卡原生编译并部署到 # /root/work/h100/bin/(交叉编译已移除),本脚本不再推送二进制。 # # 用法: # ./scripts/deploy.sh setup-ssh 一次性:生成 ed25519 key + ssh-copy-id -# ./scripts/deploy.sh 默认:推送 mission + 装 5 个 +# ./scripts/deploy.sh 默认:推送 mission + 装 7 个 # systemd 服务 + daemon-reload(不启动) # ./scripts/deploy.sh --start 部署后按依赖顺序启动服务 # ./scripts/deploy.sh status 板卡各服务状态 @@ -31,7 +32,8 @@ # /root/work/h100/data/ 数据库存储目录 (power_data.db 等) # /etc/systemd/system/ moosdb.service / pPowerManger.service / # pPowerMangerHost.service / pCCU.service / -# pCanBridge.service +# pCanBridge.service / pMotor.service / +# pELoad.service #======================================================================= set -uo pipefail @@ -45,7 +47,7 @@ DO_START=0 CCU_HOST="" CCU_PORT="" -SERVICES=(moosdb pPowerManger pPowerMangerHost pCCU pCanBridge) +SERVICES=(moosdb pPowerManger pPowerMangerHost pCCU pCanBridge pMotor pELoad) info() { printf '\033[1;36m[deploy]\033[0m %s\n' "$*"; } ok() { printf '\033[1;32m[deploy]\033[0m %s\n' "$*"; } @@ -228,10 +230,11 @@ cmd_deploy() { else echo "" info "部署完成。手动启动(或加 --start):" - echo " systemctl start moosdb pPowerManger pPowerMangerHost pCCU pCanBridge" + echo " systemctl start moosdb pPowerManger pPowerMangerHost pCCU pCanBridge pMotor pELoad" echo " 浏览器: http://${HOST}:18080 (上位机模拟)" echo " http://${HOST}:8090 (pPowerManger)" echo " http://${HOST}:8080 (pCCU)" + echo " http://${HOST}:18082 (pELoad)" fi } @@ -239,9 +242,9 @@ cmd_deploy() { # 命令: start / status / stop #------------------------------------------------------------------- cmd_start() { - info "启动服务 (moosdb -> pPowerManger / pPowerMangerHost / pCCU / pCanBridge) ..." + info "启动服务 (moosdb -> pPowerManger / pPowerMangerHost / pCCU / pCanBridge / pMotor / pELoad) ..." ${SSH} "${SSH_TARGET}" "systemctl start moosdb && sleep 1 && \ - systemctl start pPowerManger pPowerMangerHost pCCU pCanBridge" || { err "启动失败"; return 1; } + systemctl start pPowerManger pPowerMangerHost pCCU pCanBridge pMotor pELoad" || { err "启动失败"; return 1; } ok "服务已启动" cmd_status } @@ -251,7 +254,7 @@ cmd_status() { } cmd_stop() { - ${SSH} "${SSH_TARGET}" "systemctl stop pCanBridge pPowerMangerHost pPowerManger pCCU moosdb" && \ + ${SSH} "${SSH_TARGET}" "systemctl stop pELoad pMotor pCanBridge pPowerMangerHost pPowerManger pCCU moosdb" && \ ok "服务已停止" || err "停止服务失败" } diff --git a/src/pELoad/ELoad.cpp b/src/pELoad/ELoad.cpp index f215459..1a74e4c 100644 --- a/src/pELoad/ELoad.cpp +++ b/src/pELoad/ELoad.cpp @@ -87,6 +87,7 @@ bool ELoad::OnStartUp() { else if (param == "dbpath") m_dbPath = value; else if (param == "log_keep_hours") m_logKeepHours = atof(value.c_str()); else if (param == "meas_log_period_ms") m_measLogPeriodMs = atoi(value.c_str()); + else if (param == "meas_source") m_measUseFetch = (tolower(value) != "meas"); 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; @@ -317,16 +318,50 @@ void ELoad::processWebCmds() { LOG_F(INFO, "[ELoad/web] output OFF requested"); break; case WCMD_REMOTE: - if (m_scpi.isConnected()) m_dev.remote(); - break; + { + bool ok = false; + if (m_scpi.isConnected()) { + ok = m_dev.remote(); + } + { + std::lock_guard lock(m_stateMutex); + m_remoteRequested = true; + if (ok) { + m_remoteState = "remote"; + m_remoteFeed = "SYST:REM 已发送成功"; + } else { + m_remoteState = "unknown"; + m_remoteFeed = "SYST:REM 发送失败(未连接或超时)"; + } + m_remoteFeedTime = MOOSTime(false); + } + LOG_F(INFO, "[ELoad/web] remote -> %s", ok ? "ok" : "fail"); + break; + } case WCMD_LOCAL: - if (m_scpi.isConnected()) { - m_dev.local(); - m_loadOn = false; - m_remoteApplied = false; - m_cfgApplied = false; + { + bool ok = false; + if (m_scpi.isConnected()) { + ok = m_dev.local(); + } + { + std::lock_guard lock(m_stateMutex); + m_remoteRequested = false; // 保持本控,不再自动切回远控 + m_loadOn = false; // 本控下停止输出 + m_remoteApplied = false; + m_cfgApplied = false; + if (ok) { + m_remoteState = "local"; + m_remoteFeed = "SYST:LOC 已发送成功,设备处于本控"; + } else { + m_remoteState = "unknown"; + m_remoteFeed = "SYST:LOC 发送失败(未连接或超时)"; + } + m_remoteFeedTime = MOOSTime(false); + } + LOG_F(INFO, "[ELoad/web] local -> %s", ok ? "ok" : "fail"); + break; } - break; case WCMD_MODE_CC: case WCMD_MODE_CV: { bool cc = (c.type == WCMD_MODE_CC); @@ -453,12 +488,19 @@ void ELoad::doControlCycle(double now) { return; } - // 每次连接后先进入远控 - if (!m_remoteApplied) { - if (m_dev.remote()) + // 每次连接后进入远控(仅当需要远控;用户切本控后不自动切回) + if (m_remoteRequested && !m_remoteApplied) { + if (m_dev.remote()) { m_remoteApplied = true; - else + std::lock_guard lock(m_stateMutex); + if (m_remoteState != "remote") { + m_remoteState = "remote"; + m_remoteFeed = "SYST:REM 自动发送成功(连接后进入远控)"; + m_remoteFeedTime = now; + } + } else { return; + } } // 输出状态切换 @@ -508,10 +550,10 @@ void ELoad::doControlCycle(double now) { } } - // 测量轮询(每周期一条,轮转 V/I/P)——在指令下发之后 - pollDevice(now); + // 测量轮询(异步发起,不阻塞控制循环) + pollDevice(); - // 错误队列查询(1Hz,设备故障可见) + // 错误/FUNC 回读(异步,1Hz 节流) pollError(now); } @@ -659,66 +701,95 @@ double ELoad::computeCmdCurrent(double targetP) { } //--------------------------------------------------------- -// pollDevice:测量轮询(每周期一条,轮转 V/I/P) +// pollDevice:测量轮询(异步,不阻塞 100ms 控制循环) +// 每周期发起一个 MEAS 查询(V/I/P 轮转),结果由 ScpiClient IO 线程 +// 异步回调更新;上一轮未完成时跳过(节流),保证查询不堆积。 -void ELoad::pollDevice(double now) { - double v; - switch (m_measRotate % 3) { - case 0: - if (m_dev.measureVoltage(v)) { - std::lock_guard lock(m_stateMutex); - m_measV = v; m_measVTime = now; - } - break; - case 1: - if (m_dev.measureCurrent(v)) { - std::lock_guard lock(m_stateMutex); - m_measI = v; m_measITime = now; - } - break; - default: - if (m_dev.measurePower(v)) { - std::lock_guard lock(m_stateMutex); - m_measP = v; m_measPTime = now; - } - break; +namespace { +// 解析 SCPI 数字响应(如 "500.123"),空/非数字返回 false +bool parseScpiDouble(const std::string& r, double& v) { + if (r.empty()) return false; + char* end = nullptr; + double d = std::strtod(r.c_str(), &end); + if (end == r.c_str()) return false; + v = d; + return true; +} +} // namespace + +void ELoad::pollDevice() { + if (m_measPending) return; // 上一轮查询仍在 IO 线程处理中 + if (!m_scpi.isConnected()) { + std::lock_guard lock(m_stateMutex); + ++m_measFail; + m_lastMeasErr = "测量查询失败: 未连接"; + return; } - ++m_measRotate; + + m_measPending = true; + int type = m_measRotate++ % 3; // 0=VOLT 1=CURR 2=POW + // 用 FETCh(读表头缓存,不触发测量,响应快)还是 MEAS(触发测量,慢) + const char* q = ""; + if (m_measUseFetch) + q = (type == 0) ? "FETCh:VOLT?" : (type == 1) ? "FETCh:CURR?" : "FETCh:POW?"; + else + q = (type == 0) ? "MEAS:VOLT?" : (type == 1) ? "MEAS:CURR?" : "MEAS:POW?"; + + m_scpi.queryAsync(q, [this, type, q](bool ok, const std::string& resp) { + m_measPending = false; + double v = 0; + bool parsed = ok && parseScpiDouble(resp, v); + std::lock_guard lock(m_stateMutex); + if (parsed) { + ++m_measOk; + double now = MOOSTime(false); + if (type == 0) { m_measV = v; m_measVTime = now; } + else if (type == 1) { m_measI = v; m_measITime = now; } + else { m_measP = v; m_measPTime = now; } + } else { + ++m_measFail; + m_lastMeasErr = std::string(q) + " 失败: " + + (ok ? "无有效响应[" + resp + "]" : "超时或无响应"); + } + }); } //--------------------------------------------------------- -// pollError:1Hz 周期——错误队列 + FUNC/FUNC:MODE 状态回读 +// pollError:异步回读(1Hz 节流,每周期一条,轮流 FUNC/FUNC:MODE/SYST:ERR) +// 同样不阻塞控制循环。 void ELoad::pollError(double now) { if (now - m_lastErrPoll < 1.0) return; m_lastErrPoll = now; - // 环路优先与功能模式回读(FIX 防护:若被外部切成 LIST/BATT 等, - // 实时功率控制会失效,必须在网页/日志中醒目提示) - { - std::string func, mode; - bool hasFunc = m_dev.queryFunction(func); - bool hasMode = m_dev.queryFunctionMode(mode); - if (hasFunc || hasMode) { + int type = (m_measRotate + 1) % 3; // 与测量错开 + switch (type) { + case 0: + m_scpi.queryAsync("FUNC?", [this](bool ok, const std::string& r) { + if (ok) { std::lock_guard lock(m_stateMutex); m_devFunc = r; } + }); + break; + case 1: + m_scpi.queryAsync("FUNC:MODE?", [this](bool ok, const std::string& r) { + if (!ok) return; std::lock_guard lock(m_stateMutex); - if (hasFunc) m_devFunc = func; - if (hasMode) m_devFuncMode = mode; - bool warn = hasMode && (mode != "FIX"); + m_devFuncMode = r; + bool warn = (r != "FIX"); m_funcModeFixWarn = warn; - if (warn) - LOG_F(WARNING, "[ELoad] FUNC:MODE = %s(非 FIX,实时功率控制失效!)", mode.c_str()); - } + if (warn) LOG_F(WARNING, "[ELoad] FUNC:MODE = %s(非 FIX,实时功率控制失效!)", r.c_str()); + }); + break; + default: + m_scpi.queryAsync("SYST:ERR?", [this](bool ok, const std::string& r) { + if (!ok) return; + if (r.empty()) return; + if (r[0] == '0' && r.size() > 1 && r[1] == ',') return; // 无错误 + std::lock_guard lock(m_stateMutex); + m_lastError = r; + LOG_F(WARNING, "[ELoad] device error: %s", r.c_str()); + }); + break; } - - std::string err; - if (!m_dev.nextError(err)) return; - if (err.empty()) return; - // "0,..." 表示无错误 - if (err[0] == '0' && err.size() > 1 && err[1] == ',') return; - - std::lock_guard lock(m_stateMutex); - m_lastError = err; - LOG_F(WARNING, "[ELoad] device error: %s", err.c_str()); } //--------------------------------------------------------- @@ -732,7 +803,7 @@ void ELoad::dbPeriodic(double now) { if (m_measLogPeriodMs > 0) { std::lock_guard lock(m_stateMutex); double target = m_mode == "volt" ? m_voltSet : m_appliedPower; - m_db->insertMeas(m_measV, m_measI, m_measP, m_cmdCurrent, target, m_cfgApplied); + m_db->insertMeas(m_measV, m_measI, m_measP, m_cmdCurrent, m_cmdVolt, target, m_cfgApplied); } if (now - m_lastDbWrite >= 1.0) { @@ -742,7 +813,8 @@ void ELoad::dbPeriodic(double now) { std::lock_guard lock(m_stateMutex); m_db->insertStatus(connected, m_cfgApplied, m_mode, m_appliedPower, // 超时/限幅/斜率处理后的有效目标功率 - m_cmdCurrent, m_measV, m_measI, m_measP, + m_voltSet, // CV 目标电压 + m_cmdCurrent, m_cmdVolt, m_measV, m_measI, m_measP, m_devFunc.empty() ? (m_ccPriority ? "CURR" : "VOLT") : m_devFunc, m_scpi.txCount(), m_scpi.rxCount(), m_scpi.txErrorCount()); } @@ -843,6 +915,10 @@ std::string ELoad::buildSnapshot() { dev["funcMode"] = m_devFuncMode; dev["funcModeFixWarn"] = m_funcModeFixWarn ? 1 : 0; dev["funcModeValid"] = m_devFuncMode.empty() ? 0 : 1; + dev["remoteState"] = m_remoteState; // remote / local / unknown + dev["remoteFeed"] = m_remoteFeed; // 最近远控/本控指令执行结果 + dev["remoteFeedTime"] = m_remoteFeedTime; + dev["remoteFeedValid"] = m_remoteFeedTime > 0 ? 1 : 0; dev["voltLimit"] = m_voltLimit; dev["currLimit"] = m_currLimitPos; dev["currLimitNeg"] = m_currLimitNeg; @@ -857,6 +933,9 @@ std::string ELoad::buildSnapshot() { dev["measP"] = m_measP; dev["measPValid"]= m_measPTime > 0 ? 1 : 0; dev["measPAge"] = m_measPTime > 0 ? (now - m_measPTime) : -1; + dev["measOk"] = Json::Value((Json::UInt64)m_measOk); + dev["measFail"] = Json::Value((Json::UInt64)m_measFail); + dev["lastMeasErr"] = m_lastMeasErr; root["dev"] = dev; // 存储 @@ -898,7 +977,9 @@ std::pair ELoad::handleApi(const std::string& uri, con r["outp"] = data[i].outp; r["mode"] = data[i].mode; r["targetPower"] = data[i].targetPower; + r["voltSet"] = data[i].voltSet; r["cmdCurrent"] = data[i].cmdCurrent; + r["cmdVolt"] = data[i].cmdVolt; r["measVoltage"] = data[i].measVoltage; r["measCurrent"] = data[i].measCurrent; r["measPower"] = data[i].measPower; @@ -957,6 +1038,7 @@ std::pair ELoad::handleApi(const std::string& uri, con r["i"] = data[i].measCurrent; r["p"] = data[i].measPower; r["cmdI"] = data[i].cmdCurrent; + r["cmdV"] = data[i].cmdVolt; r["targetP"] = data[i].targetPower; r["outp"] = data[i].outp; rows.append(r); @@ -973,7 +1055,7 @@ std::pair ELoad::handleApi(const std::string& uri, con getQueryParam(query, "start", stStr); getQueryParam(query, "end", etStr); std::string csv = "time,meas_voltage_V,meas_current_A,meas_power_W," - "cmd_current_A,target_power_W,outp\n"; + "cmd_current_A,cmd_volt_V,target_power_W,outp\n"; if (m_db && m_db->isOpen()) { std::vector data; if (!stStr.empty() && !etStr.empty()) { @@ -986,10 +1068,10 @@ std::pair ELoad::handleApi(const std::string& uri, con } char buf[128]; for (size_t i = 0; i < data.size(); ++i) { - std::snprintf(buf, sizeof(buf), "%.6f,%.4f,%.4f,%.4f,%.4f,%.4f,%d\n", + std::snprintf(buf, sizeof(buf), "%.6f,%.4f,%.4f,%.4f,%.4f,%.4f,%.4f,%d\n", data[i].time, data[i].measVoltage, data[i].measCurrent, - data[i].measPower, data[i].cmdCurrent, data[i].targetPower, - data[i].outp); + data[i].measPower, data[i].cmdCurrent, data[i].cmdVolt, + data[i].targetPower, data[i].outp); csv += buf; } } @@ -1157,6 +1239,13 @@ bool ELoad::buildReport() { << " 指令电流=" << m_cmdCurrent << " A" << "\n"; m_msgs << "实测: V=" << m_measV << " I=" << m_measI << " P=" << m_measP << "\n"; + m_msgs << "测量查询: 成功 " << m_measOk << " 次, 失败 " << m_measFail + << " 次" + << (m_lastMeasErr.empty() ? std::string("") + : std::string(" 最近失败: ") + m_lastMeasErr) + << "\n"; + m_msgs << "远控状态: " << m_remoteState + << " (" << m_remoteFeed << ")\n"; m_msgs << "MOOS 功率消息: " << m_moosPower << " W (var=" << m_powerVar << (m_moosPowerTime > 0 ? ")" diff --git a/src/pELoad/ELoad.h b/src/pELoad/ELoad.h index 4247f5c..0972436 100644 --- a/src/pELoad/ELoad.h +++ b/src/pELoad/ELoad.h @@ -54,8 +54,8 @@ private: void applyDisable(); // 关输出 double computeTargetPower(double now); // 目标功率(含超时/斜率处理) double computeCmdCurrent(double targetP); // 功率折算电流(含限幅) - void pollDevice(double now); // 测量轮询与错误查询 - void pollError(double now); + void pollDevice(); // 异步测量轮询(不阻塞控制循环) + void pollError(double now); // 异步错误/FUNC 回读(1Hz 节流) void dbPeriodic(double now); // 状态落库 + 历史清理 void pruneHistory(double now); @@ -105,6 +105,7 @@ private: bool m_webEnable = true; double m_logKeepHours = 72; // 历史保留时长(0=永久) int m_measLogPeriodMs = 100; // 测量落库周期(默认=控制周期100ms) + bool m_measUseFetch = true; // 测量命令用 FETCh(读缓存,快);false=MEAS(触发测量,慢) //---- 设备配置值(web/配置可改;>0 表示需要下发)---- double m_voltLimit = 0; @@ -139,12 +140,22 @@ private: std::string m_lastError; double m_measV = 0, m_measI = 0, m_measP = 0; double m_measVTime = 0, m_measITime = 0, m_measPTime = 0; + // 测量查询诊断(定位"设备在输出但测量不更新") + unsigned long m_measOk = 0; // 查询成功次数 + unsigned long m_measFail = 0; // 查询失败次数 + std::string m_lastMeasErr; // 最近一次失败原因 std::string m_devFunc; // FUNC? 回读:CURR / VOLT std::string m_devFuncMode; // FUNC:MODE? 回读:FIX/LIST/... bool m_funcModeFixWarn = false; // 回读非 FIX 时告警 double m_cmdCurrent = 0; // 最近下发的电流指令(CC) double m_cmdVolt = 0; // 最近下发的电压指令(CV) + //---- 远控/本控状态(IT6000C 无 SYST:REM? 查询,按最近指令+发送结果推断)---- + bool m_remoteRequested = true; // true=需要远控(每次连接自动发 SYST:REM) + std::string m_remoteState = "unknown"; // remote / local / unknown + std::string m_remoteFeed = "-"; // 最近远控/本控指令执行结果 + double m_remoteFeedTime = 0; // 最近反馈时间 + //---- 控制线程私有(无需加锁)---- bool m_remoteApplied = false; bool m_cfgApplied = false; @@ -155,6 +166,7 @@ private: double m_lastDbWrite = 0; double m_lastPrune = 0; int m_measRotate = 0; + std::atomic m_measPending{false}; // 异步测量查询进行中(节流) std::vector m_webCmdQueue; // web 线程入队 -> 控制线程出队 std::atomic m_ctrlRunning{false}; diff --git a/src/pELoad/ELoad_Info.cpp b/src/pELoad/ELoad_Info.cpp index e550393..15094a4 100644 --- a/src/pELoad/ELoad_Info.cpp +++ b/src/pELoad/ELoad_Info.cpp @@ -81,6 +81,7 @@ void showExampleConfigAndExit() { blk(" dbpath = pELoad.db // SQLite 数据库路径 "); blk(" log_keep_hours= 72 // 历史保留时长(小时,0=永久) "); blk(" meas_log_period_ms = 100 // 实测V/I/P落库周期ms(0=仅1Hz快照) "); + blk(" meas_source = fetch // 测量命令: fetch=FETCh(快) / meas=MEAS(慢) "); blk(" web_port = 18082 // 网页端口(避开8080/8090/18080/18081) "); blk(" web_enable = true // 是否启用网页 "); blk(" logpath = pELoad.log // 日志路径 "); diff --git a/src/pELoad/driver/IT6000C.cpp b/src/pELoad/driver/IT6000C.cpp index d20fb67..dc3c325 100644 --- a/src/pELoad/driver/IT6000C.cpp +++ b/src/pELoad/driver/IT6000C.cpp @@ -13,12 +13,13 @@ bool IT6000C::identify(std::string& idn) { } bool IT6000C::remote() { - // 连接建立后必须先进入远控,否则设置命令被面板本地态拒绝 - return m_scpi.cmd("SYST:REM"); + // 连接建立后必须先进入远控,否则设置命令被面板本地态拒绝。 + // 用 cmdConfirmed 等待真实写入结果,明确反馈成功/失败。 + return m_scpi.cmdConfirmed("SYST:REM", 300); } bool IT6000C::local() { - return m_scpi.cmd("SYST:LOC"); + return m_scpi.cmdConfirmed("SYST:LOC", 300); } bool IT6000C::reset() { @@ -118,4 +119,8 @@ bool IT6000C::measureVoltage(double& v) { return queryDouble("MEAS:VOLT?", v); } bool IT6000C::measureCurrent(double& i) { return queryDouble("MEAS:CURR?", i); } bool IT6000C::measurePower(double& p) { return queryDouble("MEAS:POW?", p); } +bool IT6000C::fetchVoltage(double& v) { return queryDouble("FETCh:VOLT?", v); } +bool IT6000C::fetchCurrent(double& i) { return queryDouble("FETCh:CURR?", i); } +bool IT6000C::fetchPower(double& p) { return queryDouble("FETCh:POW?", p); } + } // namespace eload diff --git a/src/pELoad/driver/IT6000C.h b/src/pELoad/driver/IT6000C.h index fcf1b96..63fee1c 100644 --- a/src/pELoad/driver/IT6000C.h +++ b/src/pELoad/driver/IT6000C.h @@ -63,9 +63,13 @@ public: bool setPowerLimitNeg(double watt); // POW:LIM:NEG // 测量 - bool measureVoltage(double& v); // MEAS:VOLT? + bool measureVoltage(double& v); // MEAS:VOLT?(触发一次测量,较慢) bool measureCurrent(double& i); // MEAS:CURR? bool measurePower(double& p); // MEAS:POW? + // 读取表头缓存测量值(不触发测量,响应快,适合 100ms 轮询) + bool fetchVoltage(double& v); // FETCh:VOLT? + bool fetchCurrent(double& i); // FETCh:CURR? + bool fetchPower(double& p); // FETCh:POW? private: bool setDouble(const char* scpi, double v); diff --git a/src/pELoad/driver/ScpiClient.cpp b/src/pELoad/driver/ScpiClient.cpp index b637271..17491b5 100644 --- a/src/pELoad/driver/ScpiClient.cpp +++ b/src/pELoad/driver/ScpiClient.cpp @@ -279,8 +279,10 @@ void ScpiClient::runQueue() { item->done = true; ++m_txErrorCount; if (m_logSink) m_logSink(item->cmd, ""); - closeSocket(); m_queueCv.notify_all(); + // 异步回调(IO 线程内,注意不要在锁内调用) + if (item->cb) item->cb(false, ""); + closeSocket(); return; } if (item->expectReply) { @@ -295,6 +297,7 @@ void ScpiClient::runQueue() { if (!m_connected) { item->done = true; m_queueCv.notify_all(); + if (item->cb) item->cb(false, ""); return; } } @@ -304,6 +307,7 @@ void ScpiClient::runQueue() { } item->done = true; m_queueCv.notify_all(); + if (item->cb) item->cb(item->ok, item->resp); } } @@ -326,6 +330,50 @@ bool ScpiClient::cmd(const std::string& scpi) { return true; } +bool ScpiClient::cmdConfirmed(const std::string& scpi, int timeoutMs) { + std::shared_ptr item(new Work()); + item->cmd = scpi; + item->expectReply = false; + { + std::lock_guard lock(m_queueMutex); + if (m_queue.size() >= 64) { + ++m_txErrorCount; + return false; + } + m_queue.push_back(item); + } + m_queueCv.notify_all(); + + // 等待 IO 线程处理完成:done 表示已尝试写入,ok 为写入结果 + double t0 = MOOSTime(false); + { + std::unique_lock lock(m_queueMutex); + while (!item->done) { + int elapsed = static_cast((MOOSTime(false) - t0) * 1000.0); + int remain = timeoutMs - elapsed; + if (remain <= 0) break; + m_queueCv.wait_for(lock, std::chrono::milliseconds(remain < 5 ? 5 : remain)); + } + if (!item->done) { + // 超时未处理:从队列移除(若仍存在),防止后续错配 + for (std::deque >::iterator it = m_queue.begin(); + it != m_queue.end(); ++it) { + if (*it == item) { + m_queue.erase(it); + break; + } + } + ++m_errorCount; + return false; + } + } + if (!item->ok) { + ++m_txErrorCount; + return false; + } + return true; +} + bool ScpiClient::query(const std::string& scpi, std::string& resp, int timeoutMs) { // 同一时刻仅一个同步查询(避免控制线程/网页线程交叉) std::lock_guard qlock(m_queryMutex); @@ -372,4 +420,18 @@ bool ScpiClient::query(const std::string& scpi, std::string& resp, int timeoutMs return true; } +bool ScpiClient::queryAsync(const std::string& scpi, QueryCallback cb) { + std::shared_ptr item(new Work()); + item->cmd = scpi; + item->expectReply = true; + item->cb = std::move(cb); + { + std::lock_guard lock(m_queueMutex); + if (m_queue.size() >= 64) return false; + m_queue.push_back(item); + } + m_queueCv.notify_all(); + return true; +} + } // namespace eload diff --git a/src/pELoad/driver/ScpiClient.h b/src/pELoad/driver/ScpiClient.h index e3297ab..1cf3d97 100644 --- a/src/pELoad/driver/ScpiClient.h +++ b/src/pELoad/driver/ScpiClient.h @@ -50,12 +50,23 @@ public: bool isConnected() const { return m_connected; } // 写命令(无响应期待)。未连接/发送失败按错误计数并丢弃。 + // 返回 true 仅表示"已入队",不保证写入成功。 bool cmd(const std::string& scpi); + // 写命令并等待确认:阻塞到 IO 线程真正把命令写入 socket(或超时)。 + // 用于需要明确成功/失败反馈的关键指令(如 SYST:REM / SYST:LOC)。 + bool cmdConfirmed(const std::string& scpi, int timeoutMs = 300); + // 同步查询:发送 scpi 并等待一行响应。 // timeoutMs 建议不大于控制周期(100ms 循环里建议 <= 80)。 bool query(const std::string& scpi, std::string& resp, int timeoutMs = 200); + // 异步查询:发送 scpi,IO 线程收到响应后调用回调(IO 线程内)。 + // 不阻塞调用线程——用于测量轮询,避免拖慢 100ms 功率控制循环。 + // 回调注意:运行在 IO 线程,勿直接调用会阻塞的操作。 + using QueryCallback = std::function; + bool queryAsync(const std::string& scpi, QueryCallback cb); + // 统计 unsigned long txCount() const { return m_txCount; } unsigned long txErrorCount() const { return m_txErrorCount; } @@ -74,6 +85,7 @@ private: std::string resp; bool done; bool ok; + QueryCallback cb; // 异步查询回调(IO 线程调用) Work() : expectReply(false), done(false), ok(false) {} }; diff --git a/src/pELoad/pELoad.moos b/src/pELoad/pELoad.moos index 9a7136a..4444f32 100644 --- a/src/pELoad/pELoad.moos +++ b/src/pELoad/pELoad.moos @@ -73,6 +73,10 @@ ProcessConfig = pELoad // 0=关闭高频记录,仅保留 1Hz 状态快照) meas_log_period_ms = 100 + // 测量命令来源:fetch=FETCh(读设备表头缓存,不触发测量,响应快,推荐); + // meas=MEAS(每次触发设备重新测量,响应慢) + meas_source = fetch + //======== 网页 ======== // (避开 pCCU 8080 / pPowerManger 8090 / pPowerMangerHost 18080 / pMotor 18081) web_port = 18082 diff --git a/src/pELoad/store/DbStore.cpp b/src/pELoad/store/DbStore.cpp index 4bdf61e..22f427c 100644 --- a/src/pELoad/store/DbStore.cpp +++ b/src/pELoad/store/DbStore.cpp @@ -52,7 +52,9 @@ bool DbStore::open() { " outp INTEGER NOT NULL," " mode TEXT NOT NULL," " target_power REAL," + " volt_set REAL," " cmd_current REAL," + " cmd_volt REAL," " meas_voltage REAL," " meas_current REAL," " meas_power REAL," @@ -82,19 +84,42 @@ bool DbStore::open() { " meas_current REAL," " meas_power REAL," " cmd_current REAL," + " cmd_volt REAL," " target_power REAL," " outp INTEGER" ");")) return false; exec("CREATE INDEX IF NOT EXISTS idx_meas_log_time ON meas_log(time);"); + // 旧库缺列自动迁移(协议/字段演进后旧数据库可自动补齐) + ensureColumn("eload_log", "volt_set", "REAL"); + ensureColumn("eload_log", "cmd_volt", "REAL"); + ensureColumn("meas_log", "cmd_volt", "REAL"); + return prepareInsert() && prepareMeasInsert() && prepareScpiInsert(); } +// 检查列是否存在,缺失则 ALTER TABLE ADD COLUMN(旧库迁移) +bool DbStore::ensureColumn(const std::string& table, const std::string& col, const std::string& type) { + if (!m_db) return false; + std::string sql = "SELECT COUNT(*) FROM pragma_table_info('" + table + "') WHERE name='" + col + "';"; + sqlite3_stmt* stmt = nullptr; + if (sqlite3_prepare_v2(m_db, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) + return false; + int n = 0; + if (sqlite3_step(stmt) == SQLITE_ROW) n = sqlite3_column_int(stmt, 0); + sqlite3_finalize(stmt); + if (n > 0) return true; // 列已存在 + + std::string alt = "ALTER TABLE " + table + " ADD COLUMN " + col + " " + type + ";"; + return exec(alt.c_str()); +} + bool DbStore::prepareInsert() { const char* sql = - "INSERT INTO eload_log (time, connected, outp, mode, target_power, cmd_current, " - "meas_voltage, meas_current, meas_power, func_mode, tx_count, rx_count, tx_err) " - "VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13);"; + "INSERT INTO eload_log (time, connected, outp, mode, target_power, volt_set, " + "cmd_current, cmd_volt, meas_voltage, meas_current, meas_power, func_mode, " + "tx_count, rx_count, tx_err) " + "VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15);"; if (sqlite3_prepare_v2(m_db, sql, -1, &m_stmtInsert, nullptr) != SQLITE_OK) { m_lastError = sqlite3_errmsg(m_db); return false; @@ -105,7 +130,7 @@ bool DbStore::prepareInsert() { bool DbStore::prepareMeasInsert() { const char* sql = "INSERT INTO meas_log (time, meas_voltage, meas_current, meas_power, " - "cmd_current, target_power, outp) VALUES (?1,?2,?3,?4,?5,?6,?7);"; + "cmd_current, cmd_volt, target_power, outp) VALUES (?1,?2,?3,?4,?5,?6,?7,?8);"; if (sqlite3_prepare_v2(m_db, sql, -1, &m_stmtMeasInsert, nullptr) != SQLITE_OK) { m_lastError = sqlite3_errmsg(m_db); return false; @@ -144,7 +169,8 @@ void DbStore::close() { } void DbStore::insertStatus(bool connected, bool outp, const std::string& mode, - double targetPower, double cmdCurrent, + double targetPower, double voltSet, + double cmdCurrent, double cmdVolt, double measV, double measI, double measP, const std::string& funcMode, unsigned long tx, unsigned long rx, unsigned long txErr) { @@ -159,7 +185,9 @@ void DbStore::insertStatus(bool connected, bool outp, const std::string& mode, sqlite3_bind_int(m_stmtInsert, i++, outp ? 1 : 0); sqlite3_bind_text(m_stmtInsert, i++, mode.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_double(m_stmtInsert, i++, targetPower); + sqlite3_bind_double(m_stmtInsert, i++, voltSet); sqlite3_bind_double(m_stmtInsert, i++, cmdCurrent); + sqlite3_bind_double(m_stmtInsert, i++, cmdVolt); sqlite3_bind_double(m_stmtInsert, i++, measV); sqlite3_bind_double(m_stmtInsert, i++, measI); sqlite3_bind_double(m_stmtInsert, i++, measP); @@ -175,7 +203,7 @@ void DbStore::insertStatus(bool connected, bool outp, const std::string& mode, } void DbStore::insertMeas(double measV, double measI, double measP, - double cmdCurrent, double targetPower, bool outp) { + double cmdCurrent, double cmdVolt, double targetPower, bool outp) { std::lock_guard lock(m_mutex); if (!m_db || !m_stmtMeasInsert) return; @@ -186,8 +214,9 @@ void DbStore::insertMeas(double measV, double measI, double measP, sqlite3_bind_double(m_stmtMeasInsert, 3, measI); sqlite3_bind_double(m_stmtMeasInsert, 4, measP); sqlite3_bind_double(m_stmtMeasInsert, 5, cmdCurrent); - sqlite3_bind_double(m_stmtMeasInsert, 6, targetPower); - sqlite3_bind_int(m_stmtMeasInsert, 7, outp ? 1 : 0); + sqlite3_bind_double(m_stmtMeasInsert, 6, cmdVolt); + sqlite3_bind_double(m_stmtMeasInsert, 7, targetPower); + sqlite3_bind_int(m_stmtMeasInsert, 8, outp ? 1 : 0); int rc = sqlite3_step(m_stmtMeasInsert); if (rc != SQLITE_DONE) { @@ -219,8 +248,9 @@ std::vector DbStore::queryRecentStatus(int limit) { if (limit > 1000) limit = 1000; const char* sql = - "SELECT id, time, connected, outp, mode, target_power, cmd_current, " - "meas_voltage, meas_current, meas_power, func_mode, tx_count, rx_count, tx_err " + "SELECT id, time, connected, outp, mode, target_power, volt_set, " + "cmd_current, cmd_volt, meas_voltage, meas_current, meas_power, func_mode, " + "tx_count, rx_count, tx_err " "FROM eload_log ORDER BY id DESC LIMIT "; std::string q = sql + std::to_string(limit) + ";"; @@ -237,15 +267,17 @@ std::vector DbStore::queryRecentStatus(int limit) { r.outp = sqlite3_column_int(stmt, 3); r.mode = reinterpret_cast(sqlite3_column_text(stmt, 4)); r.targetPower = sqlite3_column_double(stmt, 5); - r.cmdCurrent = sqlite3_column_double(stmt, 6); - r.measVoltage = sqlite3_column_double(stmt, 7); - r.measCurrent = sqlite3_column_double(stmt, 8); - r.measPower = sqlite3_column_double(stmt, 9); - const char* fm = reinterpret_cast(sqlite3_column_text(stmt, 10)); + r.voltSet = sqlite3_column_double(stmt, 6); + r.cmdCurrent = sqlite3_column_double(stmt, 7); + r.cmdVolt = sqlite3_column_double(stmt, 8); + r.measVoltage = sqlite3_column_double(stmt, 9); + r.measCurrent = sqlite3_column_double(stmt, 10); + r.measPower = sqlite3_column_double(stmt, 11); + const char* fm = reinterpret_cast(sqlite3_column_text(stmt, 12)); r.funcMode = fm ? fm : ""; - r.txCount = sqlite3_column_int64(stmt, 11); - r.rxCount = sqlite3_column_int64(stmt, 12); - r.txErr = sqlite3_column_int64(stmt, 13); + r.txCount = sqlite3_column_int64(stmt, 13); + r.rxCount = sqlite3_column_int64(stmt, 14); + r.txErr = sqlite3_column_int64(stmt, 15); rows.push_back(std::move(r)); } sqlite3_finalize(stmt); @@ -324,7 +356,7 @@ std::vector DbStore::queryMeas(int limit) { if (limit > 5000) limit = 5000; std::string q = "SELECT id, time, meas_voltage, meas_current, meas_power, " - "cmd_current, target_power, outp FROM meas_log " + "cmd_current, cmd_volt, target_power, outp FROM meas_log " "ORDER BY id DESC LIMIT " + std::to_string(limit) + ";"; sqlite3_stmt* stmt = nullptr; @@ -340,8 +372,9 @@ std::vector DbStore::queryMeas(int limit) { r.measCurrent = sqlite3_column_double(stmt, 3); r.measPower = sqlite3_column_double(stmt, 4); r.cmdCurrent = sqlite3_column_double(stmt, 5); - r.targetPower = sqlite3_column_double(stmt, 6); - r.outp = sqlite3_column_int(stmt, 7); + r.cmdVolt = sqlite3_column_double(stmt, 6); + r.targetPower = sqlite3_column_double(stmt, 7); + r.outp = sqlite3_column_int(stmt, 8); rows.push_back(std::move(r)); } sqlite3_finalize(stmt); @@ -357,7 +390,7 @@ std::vector DbStore::queryMeasRange(double startSec, double endSec, int if (endSec <= startSec) return rows; std::string q = "SELECT id, time, meas_voltage, meas_current, meas_power, " - "cmd_current, target_power, outp FROM meas_log " + "cmd_current, cmd_volt, target_power, outp FROM meas_log " "WHERE time >= " + std::to_string(startSec) + " AND time <= " + std::to_string(endSec) + " ORDER BY id LIMIT " + std::to_string(maxRows) + ";"; @@ -375,8 +408,9 @@ std::vector DbStore::queryMeasRange(double startSec, double endSec, int r.measCurrent = sqlite3_column_double(stmt, 3); r.measPower = sqlite3_column_double(stmt, 4); r.cmdCurrent = sqlite3_column_double(stmt, 5); - r.targetPower = sqlite3_column_double(stmt, 6); - r.outp = sqlite3_column_int(stmt, 7); + r.cmdVolt = sqlite3_column_double(stmt, 6); + r.targetPower = sqlite3_column_double(stmt, 7); + r.outp = sqlite3_column_int(stmt, 8); rows.push_back(std::move(r)); } sqlite3_finalize(stmt); diff --git a/src/pELoad/store/DbStore.h b/src/pELoad/store/DbStore.h index 6453a19..eb51771 100644 --- a/src/pELoad/store/DbStore.h +++ b/src/pELoad/store/DbStore.h @@ -27,8 +27,10 @@ struct ELoadLogRow { int connected; int outp; std::string mode; // follow / manual / current / volt - double targetPower; // 目标功率 W + double targetPower; // 目标功率 W(volt 模式为 0) + double voltSet; // 目标电压 V(volt 模式) double cmdCurrent; // 指令电流 A + double cmdVolt; // 指令电压 V(volt 模式) double measVoltage; // 实测电压 V double measCurrent; // 实测电流 A double measPower; // 实测功率 W @@ -46,6 +48,7 @@ struct MeasRow { double measCurrent; double measPower; double cmdCurrent; + double cmdVolt; double targetPower; int outp; }; @@ -62,14 +65,15 @@ public: // 运行状态快照落库(控制线程周期调用) void insertStatus(bool connected, bool outp, const std::string& mode, - double targetPower, double cmdCurrent, + double targetPower, double voltSet, + double cmdCurrent, double cmdVolt, double measV, double measI, double measP, const std::string& funcMode, unsigned long tx, unsigned long rx, unsigned long txErr); // 高采样测量落库(每个控制周期调用,meas_log) void insertMeas(double measV, double measI, double measP, - double cmdCurrent, double targetPower, bool outp); + double cmdCurrent, double cmdVolt, double targetPower, bool outp); // SCPI 非常规收发落库:text 为 "CMD" 或 "CMD => RESP"(已配对) // direction: 1=命令 0=查询应答 @@ -97,6 +101,7 @@ public: private: bool exec(const char* sql); + bool ensureColumn(const std::string& table, const std::string& col, const std::string& type); bool prepareInsert(); bool prepareMeasInsert(); bool prepareScpiInsert(); diff --git a/src/pELoad/web/pages/index.h b/src/pELoad/web/pages/index.h index e639d04..9a3b27a 100644 --- a/src/pELoad/web/pages/index.h +++ b/src/pELoad/web/pages/index.h @@ -10,7 +10,11 @@ // ① 连接设备(链路绿 → 步骤2 可操作) // ② 设置参数(选环路优先 CC/CV → 控制模式/目标量 → 限值) // ③ 开启输出(确认后下发使能序列) -// 所有下发的指令均带确认对话框;按钮按当前状态禁用并提示原因。 +// 所有下发的指令均带确认对话框;按钮按当前状态禁用。 +// +// 关键设计:页面为静态骨架(输入框/按钮是稳定 DOM,永不重建), +// render() 只按 id 更新动态数值/状态/按钮禁用 —— 输入框聚焦编辑时 +// 不受 1Hz 快照刷新影响,光标稳定、输入流畅(setInput 聚焦时不覆盖)。 // // 模式说明: // CC 优先(电流型负载模拟):控制量 = 电流,功率折算 I=P/U,可跟随 MOOS @@ -58,7 +62,6 @@ input:focus{outline:none;border-color:var(--blue);} input:disabled{opacity:.4;} .inline{display:flex;align-items:center;gap:8px;margin-bottom:8px;flex-wrap:wrap;} .inline label{color:var(--dim);} -.big{font-size:22px;font-family:Consolas,monospace;} .dim{color:var(--dim);} .errline{color:var(--bad);font-family:Consolas,monospace;font-size:12px;word-break:break-all;} .okline{color:var(--ok);font-family:Consolas,monospace;font-size:12px;word-break:break-all;} @@ -113,13 +116,145 @@ td.l,th.l{text-align:left;} -
加载中...
+
+ + +
+

① 连接设备 · 以太网链路

+
设备地址-
+
连接状态未连接
+
自动重连-
+
重连次数-
+
收发统计-
+
最近收帧-
+
设备识别 *IDN?未识别
+ +
操作(指令均需确认)
+ + + + + +
+ + +
+

当前状态 · 实时测量

+
输出关
+ +
+
-
电压 V
+
-
电流 A
+
-
功率 W
+
+
数据时效-
+
测量查询-
+ +
环路优先(回读)-
+
功能模式(回读)-
+
+ + +
+

②③ 参数与输出控制

+
环路优先模式(决定控制量类型)
+ + +
+ +
+
控制模式(目标来源)
+ + + +
+
+ +
+
+ +
+
MOOS 功率消息-
+
指令电流 CURR-
+
符号约定-
+
+ + + +
输出控制
+ + +
+
+ + +
+

设备参数(限值下发)

+
限值在输出开启时统一下发,修改后需“关闭输出→重新开启”生效(或在运行中直接下发,设备实时响应)。
+
+ +
+
+ +
+
+ +
+
远控 / 本控
+
当前控制端-
+
最近指令结果-
+
IT6000C 无远控状态查询命令,此状态按最近一次远控/本控指令的发送结果推断;若面板被手动按回本控,需重新发送“远控”。
+ + +
+ + +
+

SCPI 调试(高级)

+
+
+
最近响应
+
-
+
非常规 SCPI 收发记录 (sqlite scpi_log)
+
点击“刷新记录”加载
+ +
+ + +
+

数据分析 (高采样 meas_log)

+
测量记录条数-
+
实测电压/电流/功率按控制周期(默认 100ms)落库,用于功率波形分析。可预览最近数据或导出 CSV 离线分析(Excel/Python)。
+
最近数据预览
+
点击“加载预览”
+ + +
按时间范围导出(unix 秒)
+
+ +
+
+ + +
+

历史记录 (sqlite eload_log, 1Hz)

+
记录总数-
+
点击“查询”加载
+ +
+ +