Commit 958505e3 by 树林

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

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

Co-authored-by: Cursor <cursoragent@cursor.com>
parent cafa72a3
......@@ -3,6 +3,8 @@
"port": 38080,
"platformUrl": "http://192.168.32.122:3001",
"pollMs": 2000,
"modbusAddressBase": 1,
"modbusAddressOffset": -1,
"adminUsername": "admin",
"adminPassword": "Admin@123",
"modbusHosts": [
......
......@@ -100,6 +100,8 @@ 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) */
int modbusAddressOffset = -1;
std::string adminUser = "admin";
std::string adminPass = "Admin@123";
std::vector<ModbusHost> modbusHosts;
......@@ -111,13 +113,28 @@ 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.0-modbus";
std::string version = "0.3.2-addr-base";
std::map<std::string, Session> sessions;
bool pollRunning = false;
};
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);
void saveConfigLocked();
void pushTelemetryToPlatform();
......@@ -275,6 +292,8 @@ std::string configToJsonLocked() {
std::ostringstream o;
o << "{\"ok\":true,\"listen\":\"" << jsonEscape(g.listenHost) << "\",\"port\":" << g.listenPort
<< ",\"platformUrl\":\"" << jsonEscape(g.platformUrl) << "\",\"pollMs\":" << g.pollMs
<< ",\"modbusAddressBase\":" << modbusAddressBaseLocked()
<< ",\"modbusAddressOffset\":" << g.modbusAddressOffset
<< ",\"adminUsername\":\"" << jsonEscape(g.adminUser) << "\",\"modbusHosts\":[";
for (size_t i = 0; i < g.modbusHosts.size(); ++i) {
if (i) o << ',';
......@@ -301,6 +320,8 @@ void saveConfigLocked() {
std::ostringstream o;
o << "{\n \"listen\": \"" << jsonEscape(g.listenHost) << "\",\n \"port\": " << g.listenPort
<< ",\n \"platformUrl\": \"" << jsonEscape(g.platformUrl) << "\",\n \"pollMs\": " << g.pollMs
<< ",\n \"modbusAddressBase\": " << modbusAddressBaseLocked()
<< ",\n \"modbusAddressOffset\": " << g.modbusAddressOffset
<< ",\n \"adminUsername\": \"" << jsonEscape(g.adminUser) << "\",\n \"adminPassword\": \""
<< jsonEscape(g.adminPass) << "\",\n \"modbusHosts\": [";
for (size_t i = 0; i < g.modbusHosts.size(); ++i) {
......@@ -335,6 +356,12 @@ 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
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");
if (!listen.empty()) g.listenHost = listen;
auto platform = extractString(body, "platformUrl");
......@@ -522,11 +549,13 @@ void decodePoint(const PointCfg& p, const std::vector<uint16_t>& regs, size_t of
void pollOnce() {
std::vector<ModbusHost> hosts;
std::vector<PointCfg> points;
int addrOffset = -1;
const int chunk = 100;
{
std::lock_guard<std::mutex> lock(g.mu);
hosts = g.modbusHosts;
points = g.points;
addrOffset = g.modbusAddressOffset;
}
const std::string ts = isoNow();
std::map<std::string, PointValue> nextValues;
......@@ -560,11 +589,14 @@ void pollOnce() {
if (mine.empty()) {
std::vector<uint16_t> tmp;
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;
hostLatency[h.id] = lat;
continue;
}
// flat 仍按点表逻辑地址索引;读请求地址 = 逻辑地址 + offset(默认 -1)
int minAddr = mine.front().address;
int maxEnd = mine.front().address + wordCountForType(mine.front().dataType);
for (const auto& p : mine) {
......@@ -576,9 +608,10 @@ void pollOnce() {
int chunksOk = 0, chunksFail = 0;
for (int start = minAddr; start < maxEnd;) {
int cnt = std::min(chunk, maxEnd - start);
const int pduStart = std::max(0, start + addrOffset);
std::vector<uint16_t> regs;
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) {
anyOk = true;
lastLat = lat;
......@@ -593,8 +626,8 @@ void pollOnce() {
hostOk[h.id] = anyOk;
hostLatency[h.id] = lastLat;
std::cout << "[gateway] host " << h.id << " range=[" << minAddr << "," << maxEnd
<< ") chunksOk=" << chunksOk << " fail=" << chunksFail << " flat=" << flat.size()
<< "\n"
<< ") pduOffset=" << addrOffset << " chunksOk=" << chunksOk
<< " fail=" << chunksFail << " flat=" << flat.size() << "\n"
<< std::flush;
}
......@@ -840,7 +873,8 @@ std::string buildAdminHealthJson() {
o << "{\"ok\":true,\"version\":\"" << jsonEscape(g.version)
<< "\",\"pointsVersion\":" << g.pointsVersion << ",\"listen\":\"" << jsonEscape(g.listenHost)
<< "\",\"port\":" << g.listenPort << ",\"platformUrl\":\"" << jsonEscape(g.platformUrl)
<< "\",\"modbus\":[";
<< "\",\"modbusAddressBase\":" << modbusAddressBaseLocked()
<< ",\"modbusAddressOffset\":" << g.modbusAddressOffset << ",\"modbus\":[";
for (size_t i = 0; i < g.modbusHosts.size(); ++i) {
if (i) o << ',';
o << modbusToJson(g.modbusHosts[i]);
......@@ -1048,15 +1082,16 @@ int main(int argc, char** argv) {
bool ok = false;
std::string detail;
const int pduAddr = toPduAddress(point.address);
// 无论是否 localhost,优先真实 Modbus 写(本机 Python 模拟器也听 127.0.0.1)
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";
} else {
double scale = point.scale != 0 ? point.scale : 1.0;
double raw = (value - point.bias) / scale;
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";
}
if (!ok && localhost) {
......@@ -1077,7 +1112,8 @@ int main(int argc, char** argv) {
std::ostringstream o;
o << "{\"ok\":" << (ok ? "true" : "false") << ",\"message\":\"" << jsonEscape(detail)
<< "\",\"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;
res.set_content(o.str(), "application/json; charset=utf-8");
});
......@@ -1161,6 +1197,11 @@ int main(int argc, char** argv) {
if (!platform.empty()) g.platformUrl = platform;
int poll = extractInt(req.body, "pollMs", 0);
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) {
g.modbusHosts.clear();
for (const auto& obj : splitObjects(req.body, "modbusHosts")) {
......@@ -1271,6 +1312,8 @@ 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] Modbus poll thread started\n";
if (!svr.listen(host.c_str(), port)) {
std::cerr << "[gateway] failed to bind\n";
......
......@@ -40,7 +40,7 @@ inline void wrBe16(uint8_t* p, uint16_t v) {
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,
std::vector<uint16_t>& out, int* latencyMs = nullptr) {
out.clear();
......
......@@ -62,6 +62,8 @@ a { color: var(--accent); text-decoration: none; }
padding: 8px 10px;
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 th, .table td {
border-bottom: 1px solid var(--line);
......
......@@ -33,10 +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 items = [
['服务', h.ok ? '正常' : '异常', h.ok],
['平台', h.platformUrl || '—', true],
['监听', (h.listen || '') + ':' + (h.port || ''), true],
['地址基准', addrBase, true],
['Modbus 主机', String((h.modbus || []).length), true],
['摄像头', String((h.cameras || []).length), true],
['门禁', String((h.doors || []).length), true],
......@@ -57,6 +62,13 @@ async function loadConfig() {
document.getElementById('cfgPort').value = cfgCache.port || 38080;
document.getElementById('cfgPlatform').value = cfgCache.platformUrl || '';
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 || []);
renderDevices(cfgCache);
renderPreviews(cfgCache.cameras || []);
......@@ -233,15 +245,18 @@ async function boot() {
};
document.getElementById('btnSaveNetwork').onclick = async () => {
try {
const addrBase = Number(document.getElementById('cfgAddrBase').value);
const body = {
listen: document.getElementById('cfgListen').value.trim(),
port: Number(document.getElementById('cfgPort').value),
platformUrl: document.getElementById('cfgPlatform').value.trim(),
pollMs: Number(document.getElementById('cfgPoll').value),
modbusAddressBase: addrBase === 0 ? 0 : 1,
modbusAddressOffset: addrBase === 0 ? 0 : -1,
modbusHosts: readModbusFromTable(),
};
cfgCache = await api('/admin/api/config', { method: 'PUT', body: JSON.stringify(body) });
banner('网络配置已保存(监听地址/端口变更需重启网关进程)', true);
banner('网络配置已保存(地址基准立即生效;监听地址/端口变更需重启网关进程)', true);
renderModbus(cfgCache.modbusHosts || []);
await loadHealth();
} catch (e) {
......
......@@ -38,6 +38,14 @@
<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>采集周期 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>
<h2 style="margin-top:18px">Modbus TCP 主机</h2>
<table class="table" id="modbusTable">
......
......@@ -44,6 +44,8 @@ GATEWAY_FOLDERS = {
}
BLOCK_SIZE = 2048
# 与网关 config.modbusAddressOffset 对齐:点表地址多为设备侧 1-based,寄存器按下标 = 地址 + offset
ADDRESS_OFFSET = -1
def decode_csv_bytes(raw: bytes) -> str:
......@@ -204,8 +206,12 @@ def double_to_regs(v: float) -> tuple[int, int, int, int]:
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:
addr = point.address
addr = pdu_address(point.address)
if addr < 0 or addr >= len(regs):
return
dtype = point.data_type.lower()
......@@ -285,7 +291,7 @@ def refresh_values(
continue
held = sticky.setdefault(sim.key, set())
for i, p in enumerate(sim.points):
if p.address in held:
if pdu_address(p.address) in held:
continue
# 照明 KA 保持寄存器当前值(初始 0=关),不自动翻转
if is_sticky_point(p):
......@@ -481,10 +487,19 @@ def main() -> None:
ap.add_argument("--port-103", type=int, default=1503)
ap.add_argument("--port-104", type=int, default=1504)
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()
global ADDRESS_OFFSET
ADDRESS_OFFSET = int(args.address_offset)
csv_root = find_csv_root(args.csv_root)
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}
sims = load_gateways(csv_root, ports)
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