Commit 958505e3 by 树林

fix: 网关支持 Modbus 点表地址基于 0/1 可配置

现场点表多为 1-based,读写 PDU 默认 -1,避免目标机地址与本机错一位;管理台网络配置可切换地址基准以适配不同设备。

Co-authored-by: Cursor <cursoragent@cursor.com>
parent cafa72a3
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
"port": 38080, "port": 38080,
"platformUrl": "http://192.168.32.122:3001", "platformUrl": "http://192.168.32.122:3001",
"pollMs": 2000, "pollMs": 2000,
"modbusAddressBase": 1,
"modbusAddressOffset": -1,
"adminUsername": "admin", "adminUsername": "admin",
"adminPassword": "Admin@123", "adminPassword": "Admin@123",
"modbusHosts": [ "modbusHosts": [
......
...@@ -100,6 +100,8 @@ struct AppState { ...@@ -100,6 +100,8 @@ struct AppState {
std::string listenHost = "0.0.0.0"; std::string listenHost = "0.0.0.0";
std::string platformUrl = "http://127.0.0.1:3001"; std::string platformUrl = "http://127.0.0.1:3001";
int pollMs = 2000; int pollMs = 2000;
/** 点表地址 → PDU:现场点表/设备侧常按 1-based,协议帧需 -1(如点表 68 → PDU 67) */
int modbusAddressOffset = -1;
std::string adminUser = "admin"; std::string adminUser = "admin";
std::string adminPass = "Admin@123"; std::string adminPass = "Admin@123";
std::vector<ModbusHost> modbusHosts; std::vector<ModbusHost> modbusHosts;
...@@ -111,13 +113,28 @@ struct AppState { ...@@ -111,13 +113,28 @@ 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.0-modbus"; std::string version = "0.3.2-addr-base";
std::map<std::string, Session> sessions; std::map<std::string, Session> sessions;
bool pollRunning = false; bool pollRunning = false;
}; };
AppState g; AppState g;
/** 点表逻辑地址转 Modbus PDU 地址 */
int toPduAddress(int logicalAddr) {
const int pdu = logicalAddr + g.modbusAddressOffset;
return pdu < 0 ? 0 : pdu;
}
/** UI/配置:0 = 点表即 PDU;1 = 设备点表从 1 起(offset=-1) */
int modbusAddressBaseLocked() {
return g.modbusAddressOffset == 0 ? 0 : 1;
}
void applyModbusAddressBase(int base) {
g.modbusAddressOffset = (base == 0) ? 0 : -1;
}
std::string buildSnapshotJson(bool goodOnly = false); std::string buildSnapshotJson(bool goodOnly = false);
void saveConfigLocked(); void saveConfigLocked();
void pushTelemetryToPlatform(); void pushTelemetryToPlatform();
...@@ -275,6 +292,8 @@ std::string configToJsonLocked() { ...@@ -275,6 +292,8 @@ std::string configToJsonLocked() {
std::ostringstream o; std::ostringstream o;
o << "{\"ok\":true,\"listen\":\"" << jsonEscape(g.listenHost) << "\",\"port\":" << g.listenPort o << "{\"ok\":true,\"listen\":\"" << jsonEscape(g.listenHost) << "\",\"port\":" << g.listenPort
<< ",\"platformUrl\":\"" << jsonEscape(g.platformUrl) << "\",\"pollMs\":" << g.pollMs << ",\"platformUrl\":\"" << jsonEscape(g.platformUrl) << "\",\"pollMs\":" << g.pollMs
<< ",\"modbusAddressBase\":" << modbusAddressBaseLocked()
<< ",\"modbusAddressOffset\":" << g.modbusAddressOffset
<< ",\"adminUsername\":\"" << jsonEscape(g.adminUser) << "\",\"modbusHosts\":["; << ",\"adminUsername\":\"" << jsonEscape(g.adminUser) << "\",\"modbusHosts\":[";
for (size_t i = 0; i < g.modbusHosts.size(); ++i) { for (size_t i = 0; i < g.modbusHosts.size(); ++i) {
if (i) o << ','; if (i) o << ',';
...@@ -301,6 +320,8 @@ void saveConfigLocked() { ...@@ -301,6 +320,8 @@ void saveConfigLocked() {
std::ostringstream o; std::ostringstream o;
o << "{\n \"listen\": \"" << jsonEscape(g.listenHost) << "\",\n \"port\": " << g.listenPort o << "{\n \"listen\": \"" << jsonEscape(g.listenHost) << "\",\n \"port\": " << g.listenPort
<< ",\n \"platformUrl\": \"" << jsonEscape(g.platformUrl) << "\",\n \"pollMs\": " << g.pollMs << ",\n \"platformUrl\": \"" << jsonEscape(g.platformUrl) << "\",\n \"pollMs\": " << g.pollMs
<< ",\n \"modbusAddressBase\": " << modbusAddressBaseLocked()
<< ",\n \"modbusAddressOffset\": " << g.modbusAddressOffset
<< ",\n \"adminUsername\": \"" << jsonEscape(g.adminUser) << "\",\n \"adminPassword\": \"" << ",\n \"adminUsername\": \"" << jsonEscape(g.adminUser) << "\",\n \"adminPassword\": \""
<< jsonEscape(g.adminPass) << "\",\n \"modbusHosts\": ["; << jsonEscape(g.adminPass) << "\",\n \"modbusHosts\": [";
for (size_t i = 0; i < g.modbusHosts.size(); ++i) { for (size_t i = 0; i < g.modbusHosts.size(); ++i) {
...@@ -335,6 +356,12 @@ void loadConfigFile(const std::string& path) { ...@@ -335,6 +356,12 @@ void loadConfigFile(const std::string& path) {
if (port > 0) g.listenPort = port; if (port > 0) g.listenPort = port;
int poll = extractInt(body, "pollMs", g.pollMs); int poll = extractInt(body, "pollMs", g.pollMs);
if (poll > 0) g.pollMs = poll; if (poll > 0) g.pollMs = poll;
// 优先读「基于 0/1」;兼容直接写 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);
}
auto listen = extractString(body, "listen"); auto listen = extractString(body, "listen");
if (!listen.empty()) g.listenHost = listen; if (!listen.empty()) g.listenHost = listen;
auto platform = extractString(body, "platformUrl"); auto platform = extractString(body, "platformUrl");
...@@ -522,11 +549,13 @@ void decodePoint(const PointCfg& p, const std::vector<uint16_t>& regs, size_t of ...@@ -522,11 +549,13 @@ void decodePoint(const PointCfg& p, const std::vector<uint16_t>& regs, size_t of
void pollOnce() { void pollOnce() {
std::vector<ModbusHost> hosts; std::vector<ModbusHost> hosts;
std::vector<PointCfg> points; std::vector<PointCfg> points;
int addrOffset = -1;
const int chunk = 100; const int chunk = 100;
{ {
std::lock_guard<std::mutex> lock(g.mu); std::lock_guard<std::mutex> lock(g.mu);
hosts = g.modbusHosts; hosts = g.modbusHosts;
points = g.points; points = g.points;
addrOffset = g.modbusAddressOffset;
} }
const std::string ts = isoNow(); const std::string ts = isoNow();
std::map<std::string, PointValue> nextValues; std::map<std::string, PointValue> nextValues;
...@@ -560,11 +589,14 @@ void pollOnce() { ...@@ -560,11 +589,14 @@ void pollOnce() {
if (mine.empty()) { if (mine.empty()) {
std::vector<uint16_t> tmp; std::vector<uint16_t> tmp;
int lat = 0; int lat = 0;
bool ok = modbus_tcp::readHolding(h.host, h.port, unit, 1, 1, tmp, &lat); const int probeLogical = 1;
const int probePdu = std::max(0, probeLogical + addrOffset);
bool ok = modbus_tcp::readHolding(h.host, h.port, unit, probePdu, 1, tmp, &lat);
hostOk[h.id] = ok; hostOk[h.id] = ok;
hostLatency[h.id] = lat; hostLatency[h.id] = lat;
continue; continue;
} }
// flat 仍按点表逻辑地址索引;读请求地址 = 逻辑地址 + offset(默认 -1)
int minAddr = mine.front().address; int minAddr = mine.front().address;
int maxEnd = mine.front().address + wordCountForType(mine.front().dataType); int maxEnd = mine.front().address + wordCountForType(mine.front().dataType);
for (const auto& p : mine) { for (const auto& p : mine) {
...@@ -576,9 +608,10 @@ void pollOnce() { ...@@ -576,9 +608,10 @@ void pollOnce() {
int chunksOk = 0, chunksFail = 0; int chunksOk = 0, chunksFail = 0;
for (int start = minAddr; start < maxEnd;) { for (int start = minAddr; start < maxEnd;) {
int cnt = std::min(chunk, maxEnd - start); int cnt = std::min(chunk, maxEnd - start);
const int pduStart = std::max(0, start + addrOffset);
std::vector<uint16_t> regs; std::vector<uint16_t> regs;
int lat = 0; int lat = 0;
if (modbus_tcp::readHolding(h.host, h.port, unit, start, cnt, regs, &lat) && if (modbus_tcp::readHolding(h.host, h.port, unit, pduStart, cnt, regs, &lat) &&
static_cast<int>(regs.size()) == cnt) { static_cast<int>(regs.size()) == cnt) {
anyOk = true; anyOk = true;
lastLat = lat; lastLat = lat;
...@@ -593,8 +626,8 @@ void pollOnce() { ...@@ -593,8 +626,8 @@ void pollOnce() {
hostOk[h.id] = anyOk; hostOk[h.id] = anyOk;
hostLatency[h.id] = lastLat; hostLatency[h.id] = lastLat;
std::cout << "[gateway] host " << h.id << " range=[" << minAddr << "," << maxEnd std::cout << "[gateway] host " << h.id << " range=[" << minAddr << "," << maxEnd
<< ") chunksOk=" << chunksOk << " fail=" << chunksFail << " flat=" << flat.size() << ") pduOffset=" << addrOffset << " chunksOk=" << chunksOk
<< "\n" << " fail=" << chunksFail << " flat=" << flat.size() << "\n"
<< std::flush; << std::flush;
} }
...@@ -840,7 +873,8 @@ std::string buildAdminHealthJson() { ...@@ -840,7 +873,8 @@ std::string buildAdminHealthJson() {
o << "{\"ok\":true,\"version\":\"" << jsonEscape(g.version) o << "{\"ok\":true,\"version\":\"" << jsonEscape(g.version)
<< "\",\"pointsVersion\":" << g.pointsVersion << ",\"listen\":\"" << jsonEscape(g.listenHost) << "\",\"pointsVersion\":" << g.pointsVersion << ",\"listen\":\"" << jsonEscape(g.listenHost)
<< "\",\"port\":" << g.listenPort << ",\"platformUrl\":\"" << jsonEscape(g.platformUrl) << "\",\"port\":" << g.listenPort << ",\"platformUrl\":\"" << jsonEscape(g.platformUrl)
<< "\",\"modbus\":["; << "\",\"modbusAddressBase\":" << modbusAddressBaseLocked()
<< ",\"modbusAddressOffset\":" << g.modbusAddressOffset << ",\"modbus\":[";
for (size_t i = 0; i < g.modbusHosts.size(); ++i) { for (size_t i = 0; i < g.modbusHosts.size(); ++i) {
if (i) o << ','; if (i) o << ',';
o << modbusToJson(g.modbusHosts[i]); o << modbusToJson(g.modbusHosts[i]);
...@@ -1048,15 +1082,16 @@ int main(int argc, char** argv) { ...@@ -1048,15 +1082,16 @@ int main(int argc, char** argv) {
bool ok = false; bool ok = false;
std::string detail; std::string detail;
const int pduAddr = toPduAddress(point.address);
// 无论是否 localhost,优先真实 Modbus 写(本机 Python 模拟器也听 127.0.0.1) // 无论是否 localhost,优先真实 Modbus 写(本机 Python 模拟器也听 127.0.0.1)
if (isCoil) { if (isCoil) {
ok = modbus_tcp::writeSingleCoil(host.host, host.port, point.unitId, point.address, value != 0); ok = modbus_tcp::writeSingleCoil(host.host, host.port, point.unitId, pduAddr, value != 0);
detail = ok ? "FC05 ok" : "FC05 failed"; 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); auto regVal = static_cast<uint16_t>(static_cast<int32_t>(std::lround(raw)) & 0xFFFF);
ok = modbus_tcp::writeSingleRegister(host.host, host.port, point.unitId, point.address, regVal); ok = modbus_tcp::writeSingleRegister(host.host, host.port, point.unitId, pduAddr, regVal);
detail = ok ? "FC06 ok" : "FC06 failed"; detail = ok ? "FC06 ok" : "FC06 failed";
} }
if (!ok && localhost) { if (!ok && localhost) {
...@@ -1077,7 +1112,8 @@ int main(int argc, char** argv) { ...@@ -1077,7 +1112,8 @@ int main(int argc, char** argv) {
std::ostringstream o; std::ostringstream o;
o << "{\"ok\":" << (ok ? "true" : "false") << ",\"message\":\"" << jsonEscape(detail) o << "{\"ok\":" << (ok ? "true" : "false") << ",\"message\":\"" << jsonEscape(detail)
<< "\",\"pointId\":\"" << jsonEscape(point.id) << "\",\"itemId\":\"" << jsonEscape(point.itemId) << "\",\"pointId\":\"" << jsonEscape(point.id) << "\",\"itemId\":\"" << jsonEscape(point.itemId)
<< "\",\"address\":" << point.address << ",\"value\":" << value << "}"; << "\",\"address\":" << point.address << ",\"pduAddress\":" << pduAddr
<< ",\"value\":" << value << "}";
if (!ok) res.status = 502; if (!ok) res.status = 502;
res.set_content(o.str(), "application/json; charset=utf-8"); res.set_content(o.str(), "application/json; charset=utf-8");
}); });
...@@ -1161,6 +1197,11 @@ int main(int argc, char** argv) { ...@@ -1161,6 +1197,11 @@ int main(int argc, char** argv) {
if (!platform.empty()) g.platformUrl = platform; if (!platform.empty()) g.platformUrl = platform;
int poll = extractInt(req.body, "pollMs", 0); int poll = extractInt(req.body, "pollMs", 0);
if (poll > 0) g.pollMs = poll; if (poll > 0) g.pollMs = poll;
if (req.body.find("\"modbusAddressBase\"") != std::string::npos) {
applyModbusAddressBase(extractInt(req.body, "modbusAddressBase", 1));
} else if (req.body.find("\"modbusAddressOffset\"") != std::string::npos) {
g.modbusAddressOffset = extractInt(req.body, "modbusAddressOffset", g.modbusAddressOffset);
}
if (req.body.find("\"modbusHosts\"") != std::string::npos) { if (req.body.find("\"modbusHosts\"") != std::string::npos) {
g.modbusHosts.clear(); g.modbusHosts.clear();
for (const auto& obj : splitObjects(req.body, "modbusHosts")) { for (const auto& obj : splitObjects(req.body, "modbusHosts")) {
...@@ -1271,6 +1312,8 @@ int main(int argc, char** argv) { ...@@ -1271,6 +1312,8 @@ int main(int argc, char** argv) {
std::cout << "[gateway] JhLabGateway " << g.version << " http://" << host << ":" << port << "\n"; 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] Admin UI: http://127.0.0.1:" << port << "/ui/\n";
std::cout << "[gateway] platformUrl=" << g.platformUrl << " (telemetry → /api/gateway/telemetry)\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] 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";
......
...@@ -40,7 +40,7 @@ inline void wrBe16(uint8_t* p, uint16_t v) { ...@@ -40,7 +40,7 @@ inline void wrBe16(uint8_t* p, uint16_t v) {
p[1] = static_cast<uint8_t>(v & 0xFF); p[1] = static_cast<uint8_t>(v & 0xFF);
} }
/** 读 Holding Registers;address 与主站配置一致(0-based PDU)。成功返回 true。 */ /** 读 Holding Registers;address 为 0-based PDU(调用方负责点表→PDU 偏移)。成功返回 true。 */
inline bool readHolding(const std::string& host, int port, int unitId, int address, int count, inline bool readHolding(const std::string& host, int port, int unitId, int address, int count,
std::vector<uint16_t>& out, int* latencyMs = nullptr) { std::vector<uint16_t>& out, int* latencyMs = nullptr) {
out.clear(); out.clear();
......
...@@ -62,6 +62,8 @@ a { color: var(--accent); text-decoration: none; } ...@@ -62,6 +62,8 @@ a { color: var(--accent); text-decoration: none; }
padding: 8px 10px; padding: 8px 10px;
font-size: 13px; font-size: 13px;
} }
.field-wide { grid-column: 1 / -1; }
.field .hint { font-size: 12px; color: var(--muted); line-height: 1.4; }
.table { width: 100%; border-collapse: collapse; font-size: 13px; } .table { width: 100%; border-collapse: collapse; font-size: 13px; }
.table th, .table td { .table th, .table td {
border-bottom: 1px solid var(--line); border-bottom: 1px solid var(--line);
......
...@@ -33,10 +33,15 @@ async function loadHealth() { ...@@ -33,10 +33,15 @@ async function loadHealth() {
document.getElementById('verLabel').textContent = document.getElementById('verLabel').textContent =
(h.version || '') + ' · 点表 v' + (h.pointsVersion || 0); (h.version || '') + ' · 点表 v' + (h.pointsVersion || 0);
const cards = document.getElementById('healthCards'); const cards = document.getElementById('healthCards');
const addrBase =
h.modbusAddressBase === 0 || h.modbusAddressBase === '0'
? '基于 0(PDU)'
: '基于 1(点表)';
const items = [ const items = [
['服务', h.ok ? '正常' : '异常', h.ok], ['服务', h.ok ? '正常' : '异常', h.ok],
['平台', h.platformUrl || '—', true], ['平台', h.platformUrl || '—', true],
['监听', (h.listen || '') + ':' + (h.port || ''), true], ['监听', (h.listen || '') + ':' + (h.port || ''), true],
['地址基准', addrBase, true],
['Modbus 主机', String((h.modbus || []).length), true], ['Modbus 主机', String((h.modbus || []).length), true],
['摄像头', String((h.cameras || []).length), true], ['摄像头', String((h.cameras || []).length), true],
['门禁', String((h.doors || []).length), true], ['门禁', String((h.doors || []).length), true],
...@@ -57,6 +62,13 @@ async function loadConfig() { ...@@ -57,6 +62,13 @@ async function loadConfig() {
document.getElementById('cfgPort').value = cfgCache.port || 38080; document.getElementById('cfgPort').value = cfgCache.port || 38080;
document.getElementById('cfgPlatform').value = cfgCache.platformUrl || ''; document.getElementById('cfgPlatform').value = cfgCache.platformUrl || '';
document.getElementById('cfgPoll').value = cfgCache.pollMs || 2000; document.getElementById('cfgPoll').value = cfgCache.pollMs || 2000;
const base =
cfgCache.modbusAddressBase === 0 || cfgCache.modbusAddressBase === '0'
? '0'
: cfgCache.modbusAddressOffset === 0
? '0'
: '1';
document.getElementById('cfgAddrBase').value = base;
renderModbus(cfgCache.modbusHosts || []); renderModbus(cfgCache.modbusHosts || []);
renderDevices(cfgCache); renderDevices(cfgCache);
renderPreviews(cfgCache.cameras || []); renderPreviews(cfgCache.cameras || []);
...@@ -233,15 +245,18 @@ async function boot() { ...@@ -233,15 +245,18 @@ async function boot() {
}; };
document.getElementById('btnSaveNetwork').onclick = async () => { document.getElementById('btnSaveNetwork').onclick = async () => {
try { try {
const addrBase = Number(document.getElementById('cfgAddrBase').value);
const body = { const body = {
listen: document.getElementById('cfgListen').value.trim(), listen: document.getElementById('cfgListen').value.trim(),
port: Number(document.getElementById('cfgPort').value), port: Number(document.getElementById('cfgPort').value),
platformUrl: document.getElementById('cfgPlatform').value.trim(), platformUrl: document.getElementById('cfgPlatform').value.trim(),
pollMs: Number(document.getElementById('cfgPoll').value), pollMs: Number(document.getElementById('cfgPoll').value),
modbusAddressBase: addrBase === 0 ? 0 : 1,
modbusAddressOffset: addrBase === 0 ? 0 : -1,
modbusHosts: readModbusFromTable(), modbusHosts: readModbusFromTable(),
}; };
cfgCache = await api('/admin/api/config', { method: 'PUT', body: JSON.stringify(body) }); cfgCache = await api('/admin/api/config', { method: 'PUT', body: JSON.stringify(body) });
banner('网络配置已保存(监听地址/端口变更需重启网关进程)', true); banner('网络配置已保存(地址基准立即生效;监听地址/端口变更需重启网关进程)', true);
renderModbus(cfgCache.modbusHosts || []); renderModbus(cfgCache.modbusHosts || []);
await loadHealth(); await loadHealth();
} catch (e) { } catch (e) {
......
...@@ -38,6 +38,14 @@ ...@@ -38,6 +38,14 @@
<div class="field"><label>服务端口</label><input id="cfgPort" type="number" /></div> <div class="field"><label>服务端口</label><input id="cfgPort" type="number" /></div>
<div class="field"><label>平台 URL</label><input id="cfgPlatform" /></div> <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"><label>采集周期 ms</label><input id="cfgPoll" type="number" /></div>
<div class="field field-wide">
<label>Modbus 点表地址基准</label>
<select id="cfgAddrBase">
<option value="1">基于 1(设备/点表地址 → PDU = 地址 − 1)</option>
<option value="0">基于 0(点表地址即 PDU,不做偏移)</option>
</select>
<span class="hint">多数现场设备点表从 1 起;若读到的值整体错一位,切换此项即可适配。</span>
</div>
</div> </div>
<h2 style="margin-top:18px">Modbus TCP 主机</h2> <h2 style="margin-top:18px">Modbus TCP 主机</h2>
<table class="table" id="modbusTable"> <table class="table" id="modbusTable">
......
...@@ -44,6 +44,8 @@ GATEWAY_FOLDERS = { ...@@ -44,6 +44,8 @@ GATEWAY_FOLDERS = {
} }
BLOCK_SIZE = 2048 BLOCK_SIZE = 2048
# 与网关 config.modbusAddressOffset 对齐:点表地址多为设备侧 1-based,寄存器按下标 = 地址 + offset
ADDRESS_OFFSET = -1
def decode_csv_bytes(raw: bytes) -> str: def decode_csv_bytes(raw: bytes) -> str:
...@@ -204,8 +206,12 @@ def double_to_regs(v: float) -> tuple[int, int, int, int]: ...@@ -204,8 +206,12 @@ def double_to_regs(v: float) -> tuple[int, int, int, int]:
return struct.unpack(">HHHH", b) return struct.unpack(">HHHH", b)
def pdu_address(logical: int) -> int:
return max(0, logical + ADDRESS_OFFSET)
def write_point(regs: list[int], point: PointDef, eng: float, _t: float) -> None: def write_point(regs: list[int], point: PointDef, eng: float, _t: float) -> None:
addr = point.address addr = pdu_address(point.address)
if addr < 0 or addr >= len(regs): if addr < 0 or addr >= len(regs):
return return
dtype = point.data_type.lower() dtype = point.data_type.lower()
...@@ -285,7 +291,7 @@ def refresh_values( ...@@ -285,7 +291,7 @@ def refresh_values(
continue continue
held = sticky.setdefault(sim.key, set()) held = sticky.setdefault(sim.key, set())
for i, p in enumerate(sim.points): for i, p in enumerate(sim.points):
if p.address in held: if pdu_address(p.address) in held:
continue continue
# 照明 KA 保持寄存器当前值(初始 0=关),不自动翻转 # 照明 KA 保持寄存器当前值(初始 0=关),不自动翻转
if is_sticky_point(p): if is_sticky_point(p):
...@@ -481,10 +487,19 @@ def main() -> None: ...@@ -481,10 +487,19 @@ def main() -> None:
ap.add_argument("--port-103", type=int, default=1503) ap.add_argument("--port-103", type=int, default=1503)
ap.add_argument("--port-104", type=int, default=1504) ap.add_argument("--port-104", type=int, default=1504)
ap.add_argument("--interval", type=float, default=1.0, help="刷新周期秒") ap.add_argument("--interval", type=float, default=1.0, help="刷新周期秒")
ap.add_argument(
"--address-offset",
type=int,
default=-1,
help="点表地址→PDU 偏移,需与网关 modbusAddressOffset 一致(默认 -1)",
)
args = ap.parse_args() args = ap.parse_args()
global ADDRESS_OFFSET
ADDRESS_OFFSET = int(args.address_offset)
csv_root = find_csv_root(args.csv_root) csv_root = find_csv_root(args.csv_root)
print(f"[csv] {csv_root}", flush=True) print(f"[csv] {csv_root}", flush=True)
print(f"[addr] address_offset={ADDRESS_OFFSET}(点表 N → 寄存器 {max(0, 1 + ADDRESS_OFFSET)}…)", flush=True)
ports = {"102": args.port_102, "103": args.port_103, "104": args.port_104} ports = {"102": args.port_102, "103": args.port_103, "104": args.port_104}
sims = load_gateways(csv_root, ports) sims = load_gateways(csv_root, ports)
total = sum(len(s.points) for s in sims) total = sum(len(s.points) for s in sims)
......
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