Commit f9988953 by 树林

fix: 网关禁用线圈功能码,读写统一 FC03/FC06

原 0X/-F5/Coil/Bool 写控改为 Holding FC06 写 0/1,不再下发 FC05;采集仍只用 FC03。

Co-authored-by: Cursor <cursoragent@cursor.com>
parent f29bb245
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
|---|---| |---|---|
| id | 行号(导入时可忽略,平台用 UUID) | | id | 行号(导入时可忽略,平台用 UUID) |
| Name | 点名 | | Name | 点名 |
| RegType | 现场侧类型+功能码。`3X(Input Register)` / `1X` 为只读采集;`0X(Coil Status)-F5` 为写单线圈(控制);`4X(Holding Register)-F6` 为写寄存器(控制);`-F15/-F16` 为多写。**例外**:电表 / 流量计(水)/ 能量计 点表虽常标 `-F6`,点名均为电压/电流/流量/热量等采集量,`Description` 为空,业务上按只读处理 | | RegType | 现场侧类型+功能码。网关统一 **只使用 FC03 读 / FC06 写**(不用 FC02/FC05 线圈)。点表若标 `0X…-F5` / Coil,写控时仍按 Holding 发 FC06(0/1)。`4X…-F6` 为写寄存器。**例外**:电表 / 流量计(水)/ 能量计点表虽常标 `-F6`,业务上按只读处理 |
| RegAddress | 现场侧地址 | | RegAddress | 现场侧地址 |
| DataType | `Short` / `Word` / `Float` / `Double` / `Bit` 等 | | DataType | `Short` / `Word` / `Float` / `Double` / `Bit` 等 |
| Modbus ID | 从站号(unit id) | | Modbus ID | 从站号(unit id) |
......
...@@ -31,7 +31,7 @@ struct PointCfg { ...@@ -31,7 +31,7 @@ struct PointCfg {
std::string name; std::string name;
std::string gatewayId; std::string gatewayId;
std::string regType; std::string regType;
std::string rw = "r"; // r | w | rw;F5/F6 写控点为 w,不参与周期读 std::string rw = "r"; // r | w | rw;写控点为 w,不参与周期读(统一 FC06,不用线圈)
int unitId = 1; int unitId = 1;
int address = 0; int address = 0;
std::string dataType = "Short"; std::string dataType = "Short";
...@@ -102,7 +102,7 @@ struct AppState { ...@@ -102,7 +102,7 @@ struct AppState {
int pollMs = 2000; int pollMs = 2000;
/** FC03 读:点表→PDU,现场读点常需 -1(点表 68 → PDU 67) */ /** FC03 读:点表→PDU,现场读点常需 -1(点表 68 → PDU 67) */
int modbusAddressOffset = -1; int modbusAddressOffset = -1;
/** FC05/FC06 写:与读同为 -1(点表 87 → PDU 86 → 设备侧 87);基于 0 时会写到 88 */ /** FC06 写:与读同为 -1(点表 87 → PDU 86 → 设备侧 87);基于 0 时会写到 88 */
int modbusWriteAddressOffset = -1; int modbusWriteAddressOffset = -1;
std::string adminUser = "admin"; std::string adminUser = "admin";
std::string adminPass = "Admin@123"; std::string adminPass = "Admin@123";
...@@ -115,7 +115,7 @@ struct AppState { ...@@ -115,7 +115,7 @@ struct AppState {
std::map<std::string, PointValue> values; // key = point id std::map<std::string, PointValue> values; // key = point id
std::map<std::string, bool> hostOk; std::map<std::string, bool> hostOk;
std::map<std::string, int> hostLatencyMs; std::map<std::string, int> hostLatencyMs;
std::string version = "0.3.4-write-base1"; std::string version = "0.3.5-fc06-only";
std::map<std::string, Session> sessions; std::map<std::string, Session> sessions;
bool pollRunning = false; bool pollRunning = false;
}; };
...@@ -125,7 +125,7 @@ AppState g; ...@@ -125,7 +125,7 @@ AppState g;
static int offsetFromBase(int base) { return (base == 0) ? 0 : -1; } static int offsetFromBase(int base) { return (base == 0) ? 0 : -1; }
static int baseFromOffset(int offset) { return offset == 0 ? 0 : 1; } static int baseFromOffset(int offset) { return offset == 0 ? 0 : 1; }
/** FC05/FC06 写:点表逻辑地址 → PDU(与 FC03 读偏移独立) */ /** FC06 写:点表逻辑地址 → PDU(与 FC03 读偏移独立;禁用线圈 FC02/FC05) */
int toWritePduAddress(int logicalAddr) { int toWritePduAddress(int logicalAddr) {
const int pdu = logicalAddr + g.modbusWriteAddressOffset; const int pdu = logicalAddr + g.modbusWriteAddressOffset;
return pdu < 0 ? 0 : pdu; return pdu < 0 ? 0 : pdu;
...@@ -1093,26 +1093,27 @@ int main(int argc, char** argv) { ...@@ -1093,26 +1093,27 @@ int main(int argc, char** argv) {
std::string reg = point.regType; std::string reg = point.regType;
for (char& c : reg) c = static_cast<char>(::toupper(static_cast<unsigned char>(c))); for (char& c : reg) c = static_cast<char>(::toupper(static_cast<unsigned char>(c)));
const bool isCoil = reg.find("0X") != std::string::npos || reg.find("-F5") != std::string::npos || std::string dtype = point.dataType;
reg.find("COIL") != std::string::npos; for (char& c : dtype) c = static_cast<char>(::tolower(static_cast<unsigned char>(c)));
// 现场禁止线圈功能码:原 0X/-F5/Coil/Bool 一律走 Holding FC06(写 0/1)
const bool asBool = reg.find("0X") != std::string::npos || reg.find("-F5") != std::string::npos ||
reg.find("COIL") != std::string::npos || dtype.find("bool") != std::string::npos;
const bool localhost = const bool localhost =
host.host == "127.0.0.1" || host.host == "localhost" || host.host == "::1"; host.host == "127.0.0.1" || host.host == "localhost" || host.host == "::1";
bool ok = false; bool ok = false;
std::string detail; std::string detail;
// 写地址与读地址基准独立(现场 FC03 需 -1,FC06 常需按点表原样下发)
const int pduAddr = toWritePduAddress(point.address); const int pduAddr = toWritePduAddress(point.address);
// 无论是否 localhost,优先真实 Modbus 写(本机 Python 模拟器也听 127.0.0.1) uint16_t regVal = 0;
if (isCoil) { if (asBool) {
ok = modbus_tcp::writeSingleCoil(host.host, host.port, point.unitId, pduAddr, value != 0); regVal = value != 0 ? 1 : 0;
detail = ok ? "FC05 ok" : "FC05 failed";
} else { } else {
double scale = point.scale != 0 ? point.scale : 1.0; double scale = point.scale != 0 ? point.scale : 1.0;
double raw = (value - point.bias) / scale; double raw = (value - point.bias) / scale;
auto regVal = static_cast<uint16_t>(static_cast<int32_t>(std::lround(raw)) & 0xFFFF); regVal = static_cast<uint16_t>(static_cast<int32_t>(std::lround(raw)) & 0xFFFF);
ok = modbus_tcp::writeSingleRegister(host.host, host.port, point.unitId, pduAddr, regVal);
detail = ok ? "FC06 ok" : "FC06 failed";
} }
ok = modbus_tcp::writeSingleRegister(host.host, host.port, point.unitId, pduAddr, regVal);
detail = ok ? "FC06 ok" : "FC06 failed";
if (!ok && localhost) { if (!ok && localhost) {
// 本机联调兜底:接受写入并记入值缓存,便于 UI 回读 // 本机联调兜底:接受写入并记入值缓存,便于 UI 回读
ok = true; ok = true;
...@@ -1340,6 +1341,7 @@ int main(int argc, char** argv) { ...@@ -1340,6 +1341,7 @@ int main(int argc, char** argv) {
std::cout << "[gateway] FC03 read base=" << modbusAddressBaseLocked() std::cout << "[gateway] FC03 read base=" << modbusAddressBaseLocked()
<< " offset=" << g.modbusAddressOffset << " | FC06 write base=" << " offset=" << g.modbusAddressOffset << " | FC06 write base="
<< modbusWriteAddressBaseLocked() << " offset=" << g.modbusWriteAddressOffset << "\n"; << modbusWriteAddressBaseLocked() << " offset=" << g.modbusWriteAddressOffset << "\n";
std::cout << "[gateway] Modbus: FC03/FC06 only (no FC02/FC05 coils)\n";
std::cout << "[gateway] Modbus poll thread started\n"; std::cout << "[gateway] Modbus poll thread started\n";
if (!svr.listen(host.c_str(), port)) { if (!svr.listen(host.c_str(), port)) {
std::cerr << "[gateway] failed to bind\n"; std::cerr << "[gateway] failed to bind\n";
......
...@@ -219,31 +219,7 @@ inline SOCKET connectTcp(const std::string& host, int port) { ...@@ -219,31 +219,7 @@ inline SOCKET connectTcp(const std::string& host, int port) {
} }
#endif #endif
/** FC05 写单线圈;value=true→0xFF00 */ /** FC06 写单寄存器(现场统一用寄存器读写,禁用 FC02/FC05 线圈) */
inline bool writeSingleCoil(const std::string& host, int port, int unitId, int address, bool value) {
#ifdef _WIN32
SOCKET sock = connectTcp(host, port);
if (sock == INVALID_SOCKET) return false;
static uint16_t txn = 1;
uint8_t req[12];
wrBe16(req + 0, txn++);
wrBe16(req + 2, 0);
wrBe16(req + 4, 6);
req[6] = static_cast<uint8_t>(unitId & 0xFF);
req[7] = 0x05;
wrBe16(req + 8, static_cast<uint16_t>(address));
wrBe16(req + 10, value ? 0xFF00 : 0x0000);
std::vector<uint8_t> body;
bool ok = txnExchange(sock, req, 12, body) && body.size() >= 2 && body[1] == 0x05;
closesocket(sock);
return ok;
#else
(void)host; (void)port; (void)unitId; (void)address; (void)value;
return false;
#endif
}
/** FC06 写单寄存器 */
inline bool writeSingleRegister(const std::string& host, int port, int unitId, int address, inline bool writeSingleRegister(const std::string& host, int port, int unitId, int address,
uint16_t value) { uint16_t value) {
#ifdef _WIN32 #ifdef _WIN32
......
...@@ -47,12 +47,12 @@ ...@@ -47,12 +47,12 @@
<span class="hint">周期采集 / 读保持寄存器。现场读点错一位时改此项。</span> <span class="hint">周期采集 / 读保持寄存器。现场读点错一位时改此项。</span>
</div> </div>
<div class="field field-wide"> <div class="field field-wide">
<label>FC05/FC06 写地址基准</label> <label>FC06 写地址基准</label>
<select id="cfgWriteAddrBase"> <select id="cfgWriteAddrBase">
<option value="1">基于 1(点表地址 → PDU = 地址 − 1)</option> <option value="1">基于 1(点表地址 → PDU = 地址 − 1)</option>
<option value="0">基于 0(点表地址即 PDU,不做偏移)</option> <option value="0">基于 0(点表地址即 PDU,不做偏移)</option>
</select> </select>
<span class="hint">与读默认同为基于 1。若写到偏大地址(如 87→88)选基于 1;偏小则选基于 0。</span> <span class="hint">写一律 FC06(不用 FC05 线圈)。默认同读基于 1;写偏大选基于 1,偏小选基于 0。</span>
</div> </div>
</div> </div>
<h2 style="margin-top:18px">Modbus TCP 主机</h2> <h2 style="margin-top:18px">Modbus TCP 主机</h2>
......
...@@ -209,7 +209,7 @@ devicesRouter.post('/points/sync-gateway', authRequired, requireRole('admin'), a ...@@ -209,7 +209,7 @@ devicesRouter.post('/points/sync-gateway', authRequired, requireRole('admin'), a
res.json(await syncPointsToGateway()) res.json(await syncPointsToGateway())
}) })
/** 向可写点位下发 Modbus 写(F5 线圈 / F6 寄存器) */ /** 向可写点位下发 Modbus 写(网关统一 FC06 写寄存器,不用线圈) */
devicesRouter.post('/points/:id/write', authRequired, requireRole('admin'), async (req, res) => { devicesRouter.post('/points/:id/write', authRequired, requireRole('admin'), async (req, res) => {
try { try {
const points = await listPoints() const points = await listPoints()
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment