Commit 86a12035 by 树林

fix: FC06 写地址与 FC03 读基准分离,默认写不做 -1

现场读点基于 1 已对齐,写寄存器再套 -1 会写到偏小地址(如 87→85);写通道默认基于 0,管理台可分别配置。

Co-authored-by: Cursor <cursoragent@cursor.com>
parent 958505e3
......@@ -5,6 +5,8 @@
"pollMs": 2000,
"modbusAddressBase": 1,
"modbusAddressOffset": -1,
"modbusWriteAddressBase": 0,
"modbusWriteAddressOffset": 0,
"adminUsername": "admin",
"adminPassword": "Admin@123",
"modbusHosts": [
......
......@@ -100,8 +100,10 @@ struct AppState {
std::string listenHost = "0.0.0.0";
std::string platformUrl = "http://127.0.0.1:3001";
int pollMs = 2000;
/** 点表地址 → PDU:现场点表/设备侧常按 1-based,协议帧需 -1(如点表 68 → PDU 67) */
/** FC03 读:点表→PDU,现场读点常需 -1(点表 68 → PDU 67) */
int modbusAddressOffset = -1;
/** FC05/FC06 写:与读分开;部分设备写通道不做 -1,默认 0(点表地址即 PDU) */
int modbusWriteAddressOffset = 0;
std::string adminUser = "admin";
std::string adminPass = "Admin@123";
std::vector<ModbusHost> modbusHosts;
......@@ -113,27 +115,27 @@ struct AppState {
std::map<std::string, PointValue> values; // key = point id
std::map<std::string, bool> hostOk;
std::map<std::string, int> hostLatencyMs;
std::string version = "0.3.2-addr-base";
std::string version = "0.3.3-write-addr";
std::map<std::string, Session> sessions;
bool pollRunning = false;
};
AppState g;
/** 点表逻辑地址转 Modbus PDU 地址 */
int toPduAddress(int logicalAddr) {
const int pdu = logicalAddr + g.modbusAddressOffset;
static int offsetFromBase(int base) { return (base == 0) ? 0 : -1; }
static int baseFromOffset(int offset) { return offset == 0 ? 0 : 1; }
/** FC05/FC06 写:点表逻辑地址 → PDU(与 FC03 读偏移独立) */
int toWritePduAddress(int logicalAddr) {
const int pdu = logicalAddr + g.modbusWriteAddressOffset;
return pdu < 0 ? 0 : pdu;
}
/** UI/配置:0 = 点表即 PDU;1 = 设备点表从 1 起(offset=-1) */
int modbusAddressBaseLocked() {
return g.modbusAddressOffset == 0 ? 0 : 1;
}
int modbusAddressBaseLocked() { return baseFromOffset(g.modbusAddressOffset); }
int modbusWriteAddressBaseLocked() { return baseFromOffset(g.modbusWriteAddressOffset); }
void applyModbusAddressBase(int base) {
g.modbusAddressOffset = (base == 0) ? 0 : -1;
}
void applyModbusAddressBase(int base) { g.modbusAddressOffset = offsetFromBase(base); }
void applyModbusWriteAddressBase(int base) { g.modbusWriteAddressOffset = offsetFromBase(base); }
std::string buildSnapshotJson(bool goodOnly = false);
void saveConfigLocked();
......@@ -294,6 +296,8 @@ std::string configToJsonLocked() {
<< ",\"platformUrl\":\"" << jsonEscape(g.platformUrl) << "\",\"pollMs\":" << g.pollMs
<< ",\"modbusAddressBase\":" << modbusAddressBaseLocked()
<< ",\"modbusAddressOffset\":" << g.modbusAddressOffset
<< ",\"modbusWriteAddressBase\":" << modbusWriteAddressBaseLocked()
<< ",\"modbusWriteAddressOffset\":" << g.modbusWriteAddressOffset
<< ",\"adminUsername\":\"" << jsonEscape(g.adminUser) << "\",\"modbusHosts\":[";
for (size_t i = 0; i < g.modbusHosts.size(); ++i) {
if (i) o << ',';
......@@ -322,6 +326,8 @@ void saveConfigLocked() {
<< ",\n \"platformUrl\": \"" << jsonEscape(g.platformUrl) << "\",\n \"pollMs\": " << g.pollMs
<< ",\n \"modbusAddressBase\": " << modbusAddressBaseLocked()
<< ",\n \"modbusAddressOffset\": " << g.modbusAddressOffset
<< ",\n \"modbusWriteAddressBase\": " << modbusWriteAddressBaseLocked()
<< ",\n \"modbusWriteAddressOffset\": " << g.modbusWriteAddressOffset
<< ",\n \"adminUsername\": \"" << jsonEscape(g.adminUser) << "\",\n \"adminPassword\": \""
<< jsonEscape(g.adminPass) << "\",\n \"modbusHosts\": [";
for (size_t i = 0; i < g.modbusHosts.size(); ++i) {
......@@ -356,12 +362,18 @@ void loadConfigFile(const std::string& path) {
if (port > 0) g.listenPort = port;
int poll = extractInt(body, "pollMs", g.pollMs);
if (poll > 0) g.pollMs = poll;
// 优先读「基于 0/1」;兼容直接写 offset
// 读/写地址基准可分开;优先 base,兼容直接写 offset
if (body.find("\"modbusAddressBase\"") != std::string::npos) {
applyModbusAddressBase(extractInt(body, "modbusAddressBase", 1));
} else if (body.find("\"modbusAddressOffset\"") != std::string::npos) {
g.modbusAddressOffset = extractInt(body, "modbusAddressOffset", g.modbusAddressOffset);
}
if (body.find("\"modbusWriteAddressBase\"") != std::string::npos) {
applyModbusWriteAddressBase(extractInt(body, "modbusWriteAddressBase", 0));
} else if (body.find("\"modbusWriteAddressOffset\"") != std::string::npos) {
g.modbusWriteAddressOffset =
extractInt(body, "modbusWriteAddressOffset", g.modbusWriteAddressOffset);
}
auto listen = extractString(body, "listen");
if (!listen.empty()) g.listenHost = listen;
auto platform = extractString(body, "platformUrl");
......@@ -874,7 +886,9 @@ std::string buildAdminHealthJson() {
<< "\",\"pointsVersion\":" << g.pointsVersion << ",\"listen\":\"" << jsonEscape(g.listenHost)
<< "\",\"port\":" << g.listenPort << ",\"platformUrl\":\"" << jsonEscape(g.platformUrl)
<< "\",\"modbusAddressBase\":" << modbusAddressBaseLocked()
<< ",\"modbusAddressOffset\":" << g.modbusAddressOffset << ",\"modbus\":[";
<< ",\"modbusAddressOffset\":" << g.modbusAddressOffset
<< ",\"modbusWriteAddressBase\":" << modbusWriteAddressBaseLocked()
<< ",\"modbusWriteAddressOffset\":" << g.modbusWriteAddressOffset << ",\"modbus\":[";
for (size_t i = 0; i < g.modbusHosts.size(); ++i) {
if (i) o << ',';
o << modbusToJson(g.modbusHosts[i]);
......@@ -1082,7 +1096,8 @@ int main(int argc, char** argv) {
bool ok = false;
std::string detail;
const int pduAddr = toPduAddress(point.address);
// 写地址与读地址基准独立(现场 FC03 需 -1,FC06 常需按点表原样下发)
const int pduAddr = toWritePduAddress(point.address);
// 无论是否 localhost,优先真实 Modbus 写(本机 Python 模拟器也听 127.0.0.1)
if (isCoil) {
ok = modbus_tcp::writeSingleCoil(host.host, host.port, point.unitId, pduAddr, value != 0);
......@@ -1202,6 +1217,12 @@ int main(int argc, char** argv) {
} else if (req.body.find("\"modbusAddressOffset\"") != std::string::npos) {
g.modbusAddressOffset = extractInt(req.body, "modbusAddressOffset", g.modbusAddressOffset);
}
if (req.body.find("\"modbusWriteAddressBase\"") != std::string::npos) {
applyModbusWriteAddressBase(extractInt(req.body, "modbusWriteAddressBase", 0));
} else if (req.body.find("\"modbusWriteAddressOffset\"") != std::string::npos) {
g.modbusWriteAddressOffset =
extractInt(req.body, "modbusWriteAddressOffset", g.modbusWriteAddressOffset);
}
if (req.body.find("\"modbusHosts\"") != std::string::npos) {
g.modbusHosts.clear();
for (const auto& obj : splitObjects(req.body, "modbusHosts")) {
......@@ -1312,8 +1333,9 @@ int main(int argc, char** argv) {
std::cout << "[gateway] JhLabGateway " << g.version << " http://" << host << ":" << port << "\n";
std::cout << "[gateway] Admin UI: http://127.0.0.1:" << port << "/ui/\n";
std::cout << "[gateway] platformUrl=" << g.platformUrl << " (telemetry → /api/gateway/telemetry)\n";
std::cout << "[gateway] modbusAddressBase=" << modbusAddressBaseLocked()
<< " offset=" << g.modbusAddressOffset << " (点表地址 + offset → PDU)\n";
std::cout << "[gateway] FC03 read base=" << modbusAddressBaseLocked()
<< " offset=" << g.modbusAddressOffset << " | FC06 write base="
<< modbusWriteAddressBaseLocked() << " offset=" << g.modbusWriteAddressOffset << "\n";
std::cout << "[gateway] Modbus poll thread started\n";
if (!svr.listen(host.c_str(), port)) {
std::cerr << "[gateway] failed to bind\n";
......
......@@ -33,15 +33,15 @@ async function loadHealth() {
document.getElementById('verLabel').textContent =
(h.version || '') + ' · 点表 v' + (h.pointsVersion || 0);
const cards = document.getElementById('healthCards');
const addrBase =
h.modbusAddressBase === 0 || h.modbusAddressBase === '0'
? '基于 0(PDU)'
: '基于 1(点表)';
const readBase =
h.modbusAddressBase === 0 || h.modbusAddressBase === '0' ? '读·基于0' : '读·基于1';
const writeBase =
h.modbusWriteAddressBase === 1 || h.modbusWriteAddressBase === '1' ? '写·基于1' : '写·基于0';
const items = [
['服务', h.ok ? '正常' : '异常', h.ok],
['平台', h.platformUrl || '—', true],
['监听', (h.listen || '') + ':' + (h.port || ''), true],
['地址基准', addrBase, true],
['地址基准', readBase + ' / ' + writeBase, true],
['Modbus 主机', String((h.modbus || []).length), true],
['摄像头', String((h.cameras || []).length), true],
['门禁', String((h.doors || []).length), true],
......@@ -69,6 +69,13 @@ async function loadConfig() {
? '0'
: '1';
document.getElementById('cfgAddrBase').value = base;
const writeBase =
cfgCache.modbusWriteAddressBase === 1 || cfgCache.modbusWriteAddressBase === '1'
? '1'
: cfgCache.modbusWriteAddressOffset === -1
? '1'
: '0';
document.getElementById('cfgWriteAddrBase').value = writeBase;
renderModbus(cfgCache.modbusHosts || []);
renderDevices(cfgCache);
renderPreviews(cfgCache.cameras || []);
......@@ -246,6 +253,7 @@ async function boot() {
document.getElementById('btnSaveNetwork').onclick = async () => {
try {
const addrBase = Number(document.getElementById('cfgAddrBase').value);
const writeAddrBase = Number(document.getElementById('cfgWriteAddrBase').value);
const body = {
listen: document.getElementById('cfgListen').value.trim(),
port: Number(document.getElementById('cfgPort').value),
......@@ -253,6 +261,8 @@ async function boot() {
pollMs: Number(document.getElementById('cfgPoll').value),
modbusAddressBase: addrBase === 0 ? 0 : 1,
modbusAddressOffset: addrBase === 0 ? 0 : -1,
modbusWriteAddressBase: writeAddrBase === 1 ? 1 : 0,
modbusWriteAddressOffset: writeAddrBase === 1 ? -1 : 0,
modbusHosts: readModbusFromTable(),
};
cfgCache = await api('/admin/api/config', { method: 'PUT', body: JSON.stringify(body) });
......
......@@ -39,12 +39,20 @@
<div class="field"><label>平台 URL</label><input id="cfgPlatform" /></div>
<div class="field"><label>采集周期 ms</label><input id="cfgPoll" type="number" /></div>
<div class="field field-wide">
<label>Modbus 点表地址基准</label>
<label>FC03 读地址基准</label>
<select id="cfgAddrBase">
<option value="1">基于 1(设备/点表地址 → PDU = 地址 − 1)</option>
<option value="1">基于 1(点表地址 → PDU = 地址 − 1)</option>
<option value="0">基于 0(点表地址即 PDU)</option>
</select>
<span class="hint">周期采集 / 读保持寄存器。现场读点错一位时改此项。</span>
</div>
<div class="field field-wide">
<label>FC05/FC06 写地址基准</label>
<select id="cfgWriteAddrBase">
<option value="0">基于 0(点表地址即 PDU,不做偏移)</option>
<option value="1">基于 1(点表地址 → PDU = 地址 − 1)</option>
</select>
<span class="hint">多数现场设备点表从 1 起;若读到的值整体错一位,切换此项即可适配。</span>
<span class="hint">写线圈/写寄存器与读可分开。写到了偏小的地址时选「基于 0」。</span>
</div>
</div>
<h2 style="margin-top:18px">Modbus TCP 主机</h2>
......
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