feat: PLC调试页面 + WebSocket测试套件 + curd:add立即推送修复
新增: - web_root/js/plc_debug.js: PLC 调试页面(LED/按键绑定out信号、增量刷新) - requirements.toml: Codex 项目权限预批准配置 - mimo/webserver测试工程师/: WebSocket 前后端测试套件(8脚本+README) · test_ws_raw.py — 最简 WebSocket 连通性测试 · test_debug_dc.py — datacenter 命令调试 · test_verify_push.py — 推送验证 · test_verify_immediate_push.py — 验证 curd:add 立即推送修复 · test_ws_dc_out/v2/v3.py — 数据中心→信号页端到端测试 · test_regression_final.py — 全面回归测试 修改: - ws_method.cpp: curd:add 后立即推送信号列表(+30行) - packed_fs.c: 打包新版 web 资源(PLC调试页+样式更新) - pages.js: 重构信号管理页渲染逻辑、新增PLC调试页路由 - app.js: 注册PLC调试页路由 - style.css: 新增PLC调试页样式(+106行) - index.html: 加载 plc_debug.js - plc.cpp: 微调修复
This commit is contained in:
parent
b176d957df
commit
fd34c6815f
|
|
@ -0,0 +1,198 @@
|
|||
# WebServer 前端测试程序
|
||||
|
||||
本目录存放 RTU 项目 WebServer 前端的 Python 自动化测试脚本。
|
||||
|
||||
## 📁 目录结构
|
||||
|
||||
```
|
||||
mimo/webserver测试工程师/
|
||||
├── README.md # 本文件
|
||||
├── test_ws_raw.py # 最简 WebSocket 连通性测试
|
||||
├── test_debug_dc.py # datacenter out 命令调试
|
||||
├── test_verify_push.py # 推送验证(添加后检查 out 推送)
|
||||
├── test_verify_immediate_push.py # 验证 curd:add 立即推送修复
|
||||
├── test_ws_dc_out.py # 数据中心→信号管理页 端到端测试 (v1)
|
||||
├── test_ws_dc_out_v2.py # 数据中心→信号管理页 端到端测试 (v2)
|
||||
├── test_ws_dc_out_v3.py # 数据中心→信号管理页 端到端测试 (v3)
|
||||
└── test_regression_final.py # 全面回归测试(多信号+立即推送)
|
||||
```
|
||||
|
||||
## 🔧 前置条件
|
||||
|
||||
1. **RTU 已运行**:目标设备上 RTU 进程已启动,WebSocket 服务已监听 `8000` 端口
|
||||
2. **目标 IP**:默认 `198.120.0.100`(RK3568),可在脚本中修改 `IP` 变量
|
||||
3. **Python 3**:所有脚本仅依赖 Python 标准库(`socket`, `json`, `struct`, `base64`, `os`, `time`, `sys`, `hashlib`),无需额外安装
|
||||
4. **网络可达**:测试机与目标 RTU 之间网络互通(ping 通)
|
||||
|
||||
## 📋 测试程序说明
|
||||
|
||||
### 1. test_ws_raw.py — 最简 WebSocket 连通性测试
|
||||
|
||||
**功能**:验证与 RTU 的 WebSocket 基本通信是否正常。
|
||||
|
||||
**流程**:
|
||||
1. TCP 连接 `198.120.0.100:8000`
|
||||
2. 执行 WebSocket 握手升级
|
||||
3. 发送 `get_cmds` 请求
|
||||
4. 接收并解析所有 WebSocket 帧(区分 text/binary/close/ping/pong)
|
||||
5. 打印响应内容
|
||||
|
||||
**使用方法**:
|
||||
```bash
|
||||
python3 mimo/webserver测试工程师/test_ws_raw.py
|
||||
```
|
||||
|
||||
**适用场景**:首次排查连通性问题,验证 WebSocket 协议握手是否成功。
|
||||
|
||||
---
|
||||
|
||||
### 2. test_debug_dc.py — datacenter out 命令调试
|
||||
|
||||
**功能**:调试 `datacenter out` 命令的行为,排查命令是否正常执行。
|
||||
|
||||
**流程**:
|
||||
1. WebSocket 握手后,先接收初始数据(cmd_list)
|
||||
2. 发送 `get_cmds` 获取可用命令列表
|
||||
3. 发送 `{"type":"cmd","cmd":"datacenter out"}` 注册数据中心 out 信号
|
||||
4. 逐帧解析响应,识别 `dc_data` 类型的 JSON 消息
|
||||
5. 统计各类信号数量(out/in/yk/ao/param)
|
||||
|
||||
**使用方法**:
|
||||
```bash
|
||||
python3 mimo/webserver测试工程师/test_debug_dc.py
|
||||
```
|
||||
|
||||
**适用场景**:诊断 `datacenter out` 命令无响应问题时使用。
|
||||
|
||||
---
|
||||
|
||||
### 3. test_verify_push.py — 推送验证测试
|
||||
|
||||
**功能**:注册数据中心信号后,精确验证 WebSocket 推送是否包含所有信号。
|
||||
|
||||
**流程**:
|
||||
1. 发送 `datacenter out` 注册信号
|
||||
2. 添加指定信号 `sys.ch.tcp_s0_if` 到 out 配置页
|
||||
3. 等待 3 秒后收集所有 WebSocket 推送
|
||||
4. 打印 `out` 推送中的信号列表(saddr + val)
|
||||
5. 清理:删除测试信号,关闭连接
|
||||
|
||||
**使用方法**:
|
||||
```bash
|
||||
python3 mimo/webserver测试工程师/test_verify_push.py
|
||||
```
|
||||
|
||||
**适用场景**:验证特定信号的注册和推送是否正常。
|
||||
|
||||
---
|
||||
|
||||
### 4. test_verify_immediate_push.py — curd:add 立即推送验证
|
||||
|
||||
**功能**:验证 `curd:add` 操作后服务端是否立即推送(而非等 ws_task 定时轮询)。
|
||||
|
||||
**流程**:
|
||||
1. WebSocket 握手连接
|
||||
2. 发送 `curd:add` 注册测试信号
|
||||
3. 立即监听收发,检查服务端是否在 add 后立刻推送 out 数据
|
||||
4. 对比定时推送和立即推送的延迟差异
|
||||
5. 清理测试数据
|
||||
|
||||
**使用方法**:
|
||||
```bash
|
||||
python3 mimo/webserver测试工程师/test_verify_immediate_push.py
|
||||
```
|
||||
|
||||
**适用场景**:验证 `ws_method.cpp` 中 `curd:add` 的立即推送逻辑修复。
|
||||
|
||||
---
|
||||
|
||||
### 5. test_ws_dc_out.py — 端到端测试 v1(初版)
|
||||
|
||||
**功能**:完整测试"数据中心注册信号 → 信号管理页添加 → 推送验证 → 清理"全流程。
|
||||
|
||||
**流程**:
|
||||
1. 依次执行 `datacenter out/in/yk/ao/param` 注册所有类型信号
|
||||
2. 从 `dc_data` 中选择 out 信号(优先选 ctrl_type=1/2 的直控/选控信号)
|
||||
3. 发送 `curd:add` 添加信号(2 秒间隔模拟真人操作)
|
||||
4. 等待 ws_task 定时推送 out 数据(最多 10 秒)
|
||||
5. 验证添加的信号是否出现在推送列表中
|
||||
6. 清理:发送 `curd:del` 删除测试信号
|
||||
|
||||
**技术特点**:
|
||||
- 使用 `drain_sock` 逐帧接收(区分 text/binary/close/ping 帧)
|
||||
- 关键发现文档:dc_data 通过二进制帧发送,curd:add 不会立即推送
|
||||
- `ws_recv` 函数支持 126/127 长度扩展和掩码处理
|
||||
|
||||
**使用方法**:
|
||||
```bash
|
||||
python3 mimo/webserver测试工程师/test_ws_dc_out.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. test_ws_dc_out_v2.py — 端到端测试 v2(改进版)
|
||||
|
||||
**功能**:与 v1 相同,但改进了接收方式,使用 `ws_recv_all` 原始帧集中接收。
|
||||
|
||||
**改进点**:
|
||||
- 增加了初始 `get_cmds` 握手,打印可用命令列表
|
||||
- 信号选择逻辑更宽松(直接从 out_signals 选前 3 个)
|
||||
- 增加了 fallback:当没有 out 信号时尝试添加已知信号
|
||||
- 等待超时延长到 15 秒
|
||||
|
||||
**使用方法**:
|
||||
```bash
|
||||
python3 mimo/webserver测试工程师/test_ws_dc_out_v2.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7. test_ws_dc_out_v3.py — 端到端测试 v3(修复版)
|
||||
|
||||
**功能**:与 v1/v2 相同,但**修复了终端命令的 JSON 格式**。
|
||||
|
||||
**关键修复**:
|
||||
- v1/v2 使用 `{"type":"cmd","cmd":"datacenter out"}`(错误)
|
||||
- v3 使用 `{"type":"cmd","data":"datacenter out"}`(正确)
|
||||
- 终端命令消息体中命令内容放在 `"data"` 字段,而非 `"cmd"` 字段
|
||||
|
||||
**使用方法**:
|
||||
```bash
|
||||
python3 mimo/webserver测试工程师/test_ws_dc_out_v3.py
|
||||
```
|
||||
|
||||
> **推荐使用 v3**,因为命令格式与前端实际发送的格式一致。
|
||||
|
||||
---
|
||||
|
||||
### 8. test_regression_final.py — 全面回归测试
|
||||
|
||||
**功能**:数据中心→信号管理页的全面回归测试,覆盖多信号添加、立即推送验证、删除清理全流程。
|
||||
|
||||
**流程**:
|
||||
1. WebSocket 握手连接
|
||||
2. 注册 datacenter out/in/yk/ao/param 所有信号类型
|
||||
3. 从 dc_data 中批量选取多个 out 信号
|
||||
4. 逐个添加信号并验证立即推送的完整性
|
||||
5. 统计成功/失败比例
|
||||
6. 批量删除测试信号并清理
|
||||
|
||||
**使用方法**:
|
||||
```bash
|
||||
python3 mimo/webserver测试工程师/test_regression_final.py
|
||||
```
|
||||
|
||||
**适用场景**:代码修改后的完整性回归验证,确保信号 CRUD 全流程无退化。
|
||||
|
||||
---
|
||||
|
||||
## 🔄 消息协议要点
|
||||
|
||||
| 要点 | 说明 |
|
||||
|------|------|
|
||||
| dc_data 帧类型 | **二进制帧**(opcode=0x02),非文本帧 |
|
||||
| curd:add 行为 | 添加后**不会立即推送**,需等 ws_task 定时轮询检测 has_change |
|
||||
| ws_task 周期 | 约 1 秒 |
|
||||
| 终端命令格式 | `{"type":"cmd","data":"<命令>"}` |
|
||||
| curd 消息格式 | `{"saddr":...,"signal_type":"out","curd":"add","setting_zone":"0","signal_data":""}` |
|
||||
| 操作间隔 | 添加/删除建议间隔 2 秒,模拟真人操作 |
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
#!/usr/bin/env python3
|
||||
"""调试 datacenter out 命令"""
|
||||
import socket, base64, os, struct, json, time
|
||||
|
||||
IP = '198.120.0.100'; PORT = 8000
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(10)
|
||||
sock.connect((IP, PORT))
|
||||
|
||||
key = base64.b64encode(os.urandom(16)).decode()
|
||||
req = (f"GET /ws HTTP/1.1\r\nHost: {IP}:{PORT}\r\nUpgrade: websocket\r\n"
|
||||
f"Connection: Upgrade\r\nSec-WebSocket-Key: {key}\r\n"
|
||||
f"Sec-WebSocket-Version: 13\r\n\r\n")
|
||||
sock.sendall(req.encode())
|
||||
resp = b""
|
||||
while b"\r\n\r\n" not in resp:
|
||||
resp += sock.recv(4096)
|
||||
print(f"[✓] 握手: {resp.split(b'\r\n')[0].decode()}")
|
||||
|
||||
# 先收 cmd_list
|
||||
time.sleep(0.5)
|
||||
sock.settimeout(1.0)
|
||||
all_data = b""
|
||||
try:
|
||||
while True:
|
||||
c = sock.recv(65536)
|
||||
if not c: break
|
||||
all_data += c
|
||||
except socket.timeout:
|
||||
pass
|
||||
print(f"[i] 初始数据: {len(all_data)} 字节")
|
||||
|
||||
# 发送 get_cmds
|
||||
msg = json.dumps({"type": "get_cmds"})
|
||||
L = len(msg.encode())
|
||||
mask = os.urandom(4)
|
||||
payload = msg.encode()
|
||||
masked = bytes(payload[i] ^ mask[i % 4] for i in range(L))
|
||||
frame = bytearray([0x81, 0x80 | L])
|
||||
frame.extend(mask); frame.extend(masked)
|
||||
sock.sendall(bytes(frame))
|
||||
print(f"[→] get_cmds")
|
||||
time.sleep(0.5)
|
||||
|
||||
# 收 cmd_list
|
||||
all_data = b""
|
||||
try:
|
||||
while True:
|
||||
c = sock.recv(65536)
|
||||
if not c: break
|
||||
all_data += c
|
||||
except socket.timeout:
|
||||
pass
|
||||
print(f"[i] get_cmds 响应: {len(all_data)} 字节")
|
||||
if all_data:
|
||||
# 跳过 2 字节 WS 头
|
||||
if len(all_data) > 2:
|
||||
L2 = all_data[1] & 0x7F
|
||||
print(f" 帧长度={L2}, payload={all_data[2:2+L2].decode(errors='replace')[:500]}")
|
||||
|
||||
# 现在测试 datacenter out
|
||||
cmd = json.dumps({"type": "cmd", "cmd": "datacenter out"})
|
||||
payload = cmd.encode()
|
||||
L = len(payload)
|
||||
mask = os.urandom(4)
|
||||
masked = bytes(payload[i] ^ mask[i % 4] for i in range(L))
|
||||
frame = bytearray([0x81, 0x80 | L])
|
||||
frame.extend(mask); frame.extend(masked)
|
||||
sock.sendall(bytes(frame))
|
||||
print(f"[→] datacenter out")
|
||||
time.sleep(2.0)
|
||||
|
||||
# 收响应
|
||||
all_data = b""
|
||||
try:
|
||||
while True:
|
||||
c = sock.recv(65536)
|
||||
if not c: break
|
||||
all_data += c
|
||||
print(f"[i] 收到 {len(c)} 字节")
|
||||
except socket.timeout:
|
||||
print(f"[i] 超时,共 {len(all_data)} 字节")
|
||||
|
||||
print(f"\n总响应: {len(all_data)} 字节")
|
||||
if len(all_data) == 0:
|
||||
print("[✗] datacenter out 无响应!可能命令不存在或执行失败")
|
||||
else:
|
||||
# 解析所有帧
|
||||
offset = 0
|
||||
while offset + 2 <= len(all_data):
|
||||
b0, b1 = all_data[offset], all_data[offset+1]
|
||||
opcode = b0 & 0x0F
|
||||
length = b1 & 0x7F
|
||||
offset += 2
|
||||
if length == 126:
|
||||
length = struct.unpack('>H', all_data[offset:offset+2])[0]; offset += 2
|
||||
elif length == 127:
|
||||
length = struct.unpack('>Q', all_data[offset:offset+8])[0]; offset += 8
|
||||
p = all_data[offset:offset+length]; offset += length
|
||||
try:
|
||||
j = json.loads(p.decode('utf-8'))
|
||||
print(f" [{opcode}] JSON: type={j.get('type','?')}")
|
||||
if j.get('type') == 'dc_data':
|
||||
for k in ['out','in','yk','ao','param']:
|
||||
print(f" {k}: {len(j.get(k,[]))} 个信号")
|
||||
except:
|
||||
txt = p.decode('utf-8', errors='replace')
|
||||
print(f" [{opcode}] {txt[:300]}")
|
||||
|
||||
sock.close()
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
#!/usr/bin/env python3
|
||||
"""全面回归测试: 数据中心→out 多信号添加+立即推送验证"""
|
||||
import socket, base64, os, struct, json, time
|
||||
|
||||
IP = '198.120.0.100'; PORT = 8000
|
||||
|
||||
def ws_handshake(sock):
|
||||
key = base64.b64encode(os.urandom(16)).decode()
|
||||
req = (f"GET /ws HTTP/1.1\r\nHost: {IP}:{PORT}\r\nUpgrade: websocket\r\n"
|
||||
f"Connection: Upgrade\r\nSec-WebSocket-Key: {key}\r\n"
|
||||
f"Sec-WebSocket-Version: 13\r\n\r\n")
|
||||
sock.sendall(req.encode())
|
||||
resp = b""
|
||||
while b"\r\n\r\n" not in resp:
|
||||
c = sock.recv(4096)
|
||||
if not c: break
|
||||
resp += c
|
||||
if b"101" not in resp.split(b"\r\n")[0]: raise Exception("握手失败")
|
||||
return resp
|
||||
|
||||
def ws_send(sock, msg):
|
||||
if isinstance(msg, dict): msg = json.dumps(msg, ensure_ascii=False)
|
||||
p = msg.encode(); L = len(p); mask = os.urandom(4)
|
||||
m = bytes(p[i] ^ mask[i%4] for i in range(L))
|
||||
f = bytearray([0x81])
|
||||
if L<126: f.append(0x80|L)
|
||||
elif L<65536: f.append(0x80|126); f.extend(struct.pack('>H',L))
|
||||
else: f.append(0x80|127); f.extend(struct.pack('>Q',L))
|
||||
f.extend(mask); f.extend(m); sock.sendall(bytes(f))
|
||||
|
||||
def ws_recv_all(sock, t=2.0):
|
||||
sock.settimeout(t); data=b""
|
||||
try:
|
||||
while True:
|
||||
c=sock.recv(65536)
|
||||
if not c: break
|
||||
data+=c
|
||||
except socket.timeout: pass
|
||||
msgs=[]; offset=0
|
||||
while offset+2<=len(data):
|
||||
b0,b1=data[offset],data[offset+1]; op=b0&0xF; L=b1&0x7F; offset+=2
|
||||
if L==126: L=struct.unpack('>H',data[offset:offset+2])[0]; offset+=2
|
||||
elif L==127: L=struct.unpack('>Q',data[offset:offset+8])[0]; offset+=8
|
||||
if op in (1,2) and offset+L<=len(data):
|
||||
p=data[offset:offset+L]
|
||||
try: msgs.append(json.loads(p.decode()))
|
||||
except: msgs.append(p.decode(errors='replace'))
|
||||
offset+=L
|
||||
return msgs
|
||||
|
||||
def find_out_push(msgs):
|
||||
for msg in msgs:
|
||||
if isinstance(msg, dict) and isinstance(msg.get('out'), list):
|
||||
return msg['out']
|
||||
return None
|
||||
|
||||
print("="*70)
|
||||
print(" 全面回归测试 — 数据中心→out 添加信号")
|
||||
print("="*70)
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(10); sock.connect((IP,PORT))
|
||||
ws_handshake(sock); print("[✓] 连接成功")
|
||||
|
||||
# 1. 注册 dc 信号
|
||||
ws_send(sock, {"type":"cmd","data":"datacenter out"})
|
||||
time.sleep(1); ws_recv_all(sock,1)
|
||||
|
||||
# 2. 选择 3 个不同前缀的信号测试
|
||||
targets = [
|
||||
"sys.ch.tcp_s0_if",
|
||||
"sys.ch.uart0_if",
|
||||
"iec.run_cnt"
|
||||
]
|
||||
expected = set(targets)
|
||||
|
||||
# 3. 逐个添加,验证每个都能收到立即推送
|
||||
print(f"\n--- 添加 {len(targets)} 个信号 ---")
|
||||
all_ok = True
|
||||
for i, t in enumerate(targets):
|
||||
ws_send(sock, {"saddr":t,"signal_type":"out","curd":"add","setting_zone":"0","signal_data":""})
|
||||
time.sleep(0.4)
|
||||
msgs = ws_recv_all(sock, 2)
|
||||
out_data = find_out_push(msgs)
|
||||
if out_data:
|
||||
saddr_set = {o['saddr'] for o in out_data}
|
||||
ok = t in saddr_set
|
||||
status = "✅" if ok else "❌"
|
||||
print(f" [{i+1}] {t} add → 推送 {len(out_data)}个信号 {status}")
|
||||
if not ok:
|
||||
all_ok = False
|
||||
print(f" 推送内容: {saddr_set}")
|
||||
else:
|
||||
print(f" [{i+1}] {t} add → ❌ 无推送")
|
||||
all_ok = False
|
||||
|
||||
# 4. 最终验证 — 检查所有 3 个是否都在列表中
|
||||
print(f"\n--- 最终验证 ---")
|
||||
ws_send(sock, {"type":"cmd","data":"datacenter out"})
|
||||
time.sleep(1); msgs = ws_recv_all(sock, 2)
|
||||
# 忽略 dc_data,看有没有 out 推送
|
||||
out_data = None
|
||||
for msg in msgs:
|
||||
if isinstance(msg, dict) and isinstance(msg.get('out'), list):
|
||||
out_data = msg['out']
|
||||
if not out_data:
|
||||
# 再等 ws_task
|
||||
time.sleep(3)
|
||||
msgs = ws_recv_all(sock, 3)
|
||||
out_data = find_out_push(msgs)
|
||||
|
||||
if out_data:
|
||||
saddr_set = {o['saddr'] for o in out_data}
|
||||
all_found = expected.issubset(saddr_set)
|
||||
print(f" out已注册: {len(out_data)}个 → 目标全部找到: {'✅' if all_found else '❌'}")
|
||||
if not all_found:
|
||||
print(f" 缺少: {expected - saddr_set}")
|
||||
else:
|
||||
print(" ❌ 未收到 out 推送")
|
||||
|
||||
# 5. 清理
|
||||
print(f"\n--- 清理 ---")
|
||||
for t in targets:
|
||||
ws_send(sock, {"saddr":t,"signal_type":"out","curd":"del","setting_zone":"0","signal_data":""})
|
||||
time.sleep(0.3)
|
||||
msgs = ws_recv_all(sock, 1)
|
||||
out_data = find_out_push(msgs)
|
||||
cnt = len(out_data) if out_data else '?'
|
||||
print(f" del {t} → 剩余 {cnt} 个")
|
||||
|
||||
print(f"\n{'✅ 全部通过!' if all_ok else '❌ 存在失败'}")
|
||||
try: sock.sendall(b'\x88\x80'+os.urandom(4))
|
||||
except: pass
|
||||
sock.close()
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
#!/usr/bin/env python3
|
||||
"""验证 curd:add 立即推送修复"""
|
||||
import socket, base64, os, struct, json, time
|
||||
|
||||
IP = '198.120.0.100'; PORT = 8000
|
||||
|
||||
def ws_handshake(sock):
|
||||
key = base64.b64encode(os.urandom(16)).decode()
|
||||
req = (f"GET /ws HTTP/1.1\r\nHost: {IP}:{PORT}\r\nUpgrade: websocket\r\n"
|
||||
f"Connection: Upgrade\r\nSec-WebSocket-Key: {key}\r\n"
|
||||
f"Sec-WebSocket-Version: 13\r\n\r\n")
|
||||
sock.sendall(req.encode())
|
||||
resp = b""
|
||||
while b"\r\n\r\n" not in resp:
|
||||
c = sock.recv(4096)
|
||||
if not c: break
|
||||
resp += c
|
||||
return resp
|
||||
|
||||
def ws_send(sock, msg):
|
||||
if isinstance(msg, dict): msg = json.dumps(msg, ensure_ascii=False)
|
||||
p = msg.encode(); L = len(p); mask = os.urandom(4)
|
||||
m = bytes(p[i] ^ mask[i%4] for i in range(L))
|
||||
f = bytearray([0x81])
|
||||
if L<126: f.append(0x80|L)
|
||||
elif L<65536: f.append(0x80|126); f.extend(struct.pack('>H',L))
|
||||
else: f.append(0x80|127); f.extend(struct.pack('>Q',L))
|
||||
f.extend(mask); f.extend(m); sock.sendall(bytes(f))
|
||||
|
||||
def ws_recv_all(sock, t=2.0):
|
||||
sock.settimeout(t); data=b""
|
||||
try:
|
||||
while True:
|
||||
c=sock.recv(65536)
|
||||
if not c: break
|
||||
data+=c
|
||||
except socket.timeout: pass
|
||||
msgs=[]; offset=0
|
||||
while offset+2<=len(data):
|
||||
b0,b1=data[offset],data[offset+1]; op=b0&0xF; L=b1&0x7F; offset+=2
|
||||
if L==126: L=struct.unpack('>H',data[offset:offset+2])[0]; offset+=2
|
||||
elif L==127: L=struct.unpack('>Q',data[offset:offset+8])[0]; offset+=8
|
||||
if op in (1,2) and offset+L<=len(data):
|
||||
p=data[offset:offset+L]
|
||||
try: msgs.append(json.loads(p.decode()))
|
||||
except: msgs.append(p.decode(errors='replace'))
|
||||
offset+=L
|
||||
return msgs
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(10); sock.connect((IP,PORT))
|
||||
ws_handshake(sock); print("[✓] 握手成功")
|
||||
|
||||
# 注册 dc out
|
||||
ws_send(sock, {"type":"cmd","data":"datacenter out"})
|
||||
time.sleep(1); ws_recv_all(sock,1)
|
||||
|
||||
# 测试立即推送
|
||||
target = "sys.ch.tcp_s0_if"
|
||||
print(f"\n[→] curd:add saddr={target}")
|
||||
ws_send(sock, {"saddr":target,"signal_type":"out","curd":"add","setting_zone":"0","signal_data":""})
|
||||
|
||||
# 立即检查推送(100ms 内应有立即推送)
|
||||
time.sleep(0.3)
|
||||
msgs = ws_recv_all(sock, 2)
|
||||
found = False
|
||||
for msg in msgs:
|
||||
if isinstance(msg, dict) and 'out' in msg and isinstance(msg['out'], list):
|
||||
elapsed = "立即" # curd:add后直接推送
|
||||
print(f"[✓] add后即收到 out 推送 ({elapsed}): {len(msg['out'])} 个信号")
|
||||
for o in msg['out']:
|
||||
print(f" saddr={o.get('saddr')} val={o.get('val')}")
|
||||
found = True
|
||||
|
||||
if not found:
|
||||
print("[✗] add后未立即收到推送,可能需要等待 ws_task 轮询")
|
||||
time.sleep(2)
|
||||
msgs = ws_recv_all(sock, 3)
|
||||
for msg in msgs:
|
||||
if isinstance(msg, dict) and 'out' in msg:
|
||||
print(f"[i] 延迟收到推送: {len(msg['out'])} 个信号")
|
||||
|
||||
# 清理
|
||||
ws_send(sock, {"saddr":target,"signal_type":"out","curd":"del","setting_zone":"0","signal_data":""})
|
||||
time.sleep(0.5); ws_recv_all(sock, 1)
|
||||
try: sock.sendall(b'\x88\x80'+os.urandom(4))
|
||||
except: pass
|
||||
sock.close()
|
||||
print("\n[✓] 测试完成")
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
#!/usr/bin/env python3
|
||||
"""精确验证: 添加后检查 out 推送是否包含所有信号"""
|
||||
import socket, base64, os, struct, json, time
|
||||
|
||||
IP = '198.120.0.100'; PORT = 8000
|
||||
|
||||
def ws_handshake(sock):
|
||||
key = base64.b64encode(os.urandom(16)).decode()
|
||||
req = (f"GET /ws HTTP/1.1\r\nHost: {IP}:{PORT}\r\nUpgrade: websocket\r\n"
|
||||
f"Connection: Upgrade\r\nSec-WebSocket-Key: {key}\r\n"
|
||||
f"Sec-WebSocket-Version: 13\r\n\r\n")
|
||||
sock.sendall(req.encode())
|
||||
resp = b""
|
||||
while b"\r\n\r\n" not in resp:
|
||||
c = sock.recv(4096)
|
||||
if not c: break
|
||||
resp += c
|
||||
return resp
|
||||
|
||||
def ws_send(sock, msg):
|
||||
if isinstance(msg, dict): msg = json.dumps(msg, ensure_ascii=False)
|
||||
p = msg.encode(); L = len(p); mask = os.urandom(4)
|
||||
m = bytes(p[i] ^ mask[i%4] for i in range(L))
|
||||
f = bytearray([0x81])
|
||||
if L<126: f.append(0x80|L)
|
||||
elif L<65536: f.append(0x80|126); f.extend(struct.pack('>H',L))
|
||||
else: f.append(0x80|127); f.extend(struct.pack('>Q',L))
|
||||
f.extend(mask); f.extend(m); sock.sendall(bytes(f))
|
||||
|
||||
def ws_recv_all(sock, t=2.0):
|
||||
sock.settimeout(t); data=b""
|
||||
try:
|
||||
while True:
|
||||
c=sock.recv(65536)
|
||||
if not c: break
|
||||
data+=c
|
||||
except socket.timeout: pass
|
||||
msgs=[]; offset=0
|
||||
while offset+2<=len(data):
|
||||
b0,b1=data[offset],data[offset+1]; op=b0&0xF; L=b1&0x7F; offset+=2
|
||||
if L==126: L=struct.unpack('>H',data[offset:offset+2])[0]; offset+=2
|
||||
elif L==127: L=struct.unpack('>Q',data[offset:offset+8])[0]; offset+=8
|
||||
if op in (1,2) and offset+L<=len(data):
|
||||
p=data[offset:offset+L]
|
||||
try: msgs.append(json.loads(p.decode()))
|
||||
except: msgs.append(p.decode(errors='replace'))
|
||||
offset+=L
|
||||
return msgs
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(10); sock.connect((IP,PORT))
|
||||
ws_handshake(sock)
|
||||
|
||||
# 先注册 dc out 信号
|
||||
ws_send(sock, {"type":"cmd","data":"datacenter out"})
|
||||
time.sleep(1); ws_recv_all(sock,1)
|
||||
|
||||
# 选择一个信号添加
|
||||
target = "sys.ch.tcp_s0_if"
|
||||
print(f"添加信号: {target}")
|
||||
ws_send(sock, {"saddr":target,"signal_type":"out","curd":"add","setting_zone":"0","signal_data":""})
|
||||
|
||||
# 等待推送
|
||||
time.sleep(3)
|
||||
msgs = ws_recv_all(sock, 5)
|
||||
for msg in msgs:
|
||||
if isinstance(msg, dict) and 'out' in msg and isinstance(msg['out'], list):
|
||||
print(f"out 推送: {len(msg['out'])} 个信号")
|
||||
for o in msg['out']:
|
||||
print(f" saddr={o.get('saddr')} val={o.get('val')}")
|
||||
|
||||
# 清理
|
||||
ws_send(sock, {"saddr":target,"signal_type":"out","curd":"del","setting_zone":"0","signal_data":""})
|
||||
time.sleep(0.5); ws_recv_all(sock, 1)
|
||||
try: sock.sendall(b'\x88\x80'+os.urandom(4))
|
||||
except: pass
|
||||
sock.close()
|
||||
print("done")
|
||||
|
|
@ -0,0 +1,374 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
数据中心 → 信号管理页面 (out) 添加信号功能测试
|
||||
测试 RK3568 远端 RTU 的 WebSocket 前端功能
|
||||
|
||||
关键发现:
|
||||
- dc_data 通过二进制 WebSocket 帧发送(ws_send_binary)
|
||||
- curd:add 后不会立即推送,需要等 ws_task 定时轮询(约 1 秒周期)
|
||||
- curd:del 后会立即推送
|
||||
"""
|
||||
import socket, base64, hashlib, struct, json, os, time, sys
|
||||
|
||||
IP = '198.120.0.100'
|
||||
PORT = 8000
|
||||
|
||||
# =========================== WebSocket 基础工具 ===========================
|
||||
|
||||
def ws_handshake(sock):
|
||||
"""执行 WebSocket 握手"""
|
||||
key = base64.b64encode(os.urandom(16)).decode()
|
||||
req = (
|
||||
f"GET /ws HTTP/1.1\r\n"
|
||||
f"Host: {IP}:{PORT}\r\n"
|
||||
f"Upgrade: websocket\r\n"
|
||||
f"Connection: Upgrade\r\n"
|
||||
f"Sec-WebSocket-Key: {key}\r\n"
|
||||
f"Sec-WebSocket-Version: 13\r\n"
|
||||
f"\r\n"
|
||||
)
|
||||
sock.sendall(req.encode())
|
||||
resp = b""
|
||||
while b"\r\n\r\n" not in resp:
|
||||
chunk = sock.recv(4096)
|
||||
if not chunk:
|
||||
raise Exception("Handshake failed: no response")
|
||||
resp += chunk
|
||||
if b"101" not in resp.split(b"\r\n")[0]:
|
||||
raise Exception(f"Handshake failed: {resp.split(b'\r\n')[0].decode()}")
|
||||
print("[✓] WebSocket 握手成功")
|
||||
|
||||
def ws_send(sock, msg):
|
||||
"""发送带掩码的 WebSocket 文本帧"""
|
||||
if isinstance(msg, dict):
|
||||
msg = json.dumps(msg)
|
||||
b = msg.encode()
|
||||
L = len(b)
|
||||
mask = os.urandom(4)
|
||||
masked = bytes(b[i] ^ mask[i % 4] for i in range(L))
|
||||
frame = bytearray([0x81]) # FIN + text opcode
|
||||
if L < 126:
|
||||
frame.append(0x80 | L)
|
||||
elif L < 65536:
|
||||
frame.append(0x80 | 126)
|
||||
frame.extend(struct.pack('>H', L))
|
||||
else:
|
||||
frame.append(0x80 | 127)
|
||||
frame.extend(struct.pack('>Q', L))
|
||||
frame.extend(mask)
|
||||
frame.extend(masked)
|
||||
sock.sendall(bytes(frame))
|
||||
|
||||
def ws_recv(sock, timeout=5.0):
|
||||
"""接收一个 WebSocket 帧,返回 (opcode, payload_bytes)"""
|
||||
sock.settimeout(timeout)
|
||||
try:
|
||||
header = sock.recv(2)
|
||||
if len(header) < 2:
|
||||
return None, None
|
||||
opcode = header[0] & 0x0F
|
||||
masked = (header[1] & 0x80) != 0
|
||||
length = header[1] & 0x7F
|
||||
if length == 126:
|
||||
length = struct.unpack('>H', sock.recv(2))[0]
|
||||
elif length == 127:
|
||||
length = struct.unpack('>Q', sock.recv(8))[0]
|
||||
if masked:
|
||||
mask_key = sock.recv(4)
|
||||
payload = b""
|
||||
while len(payload) < length:
|
||||
chunk = sock.recv(min(length - len(payload), 65536))
|
||||
if not chunk:
|
||||
break
|
||||
payload += chunk
|
||||
if masked:
|
||||
payload = bytes(payload[i] ^ mask_key[i % 4] for i in range(len(payload)))
|
||||
return opcode, payload
|
||||
except socket.timeout:
|
||||
return None, None
|
||||
except Exception as e:
|
||||
print(f" [!] recv error: {e}")
|
||||
return None, None
|
||||
|
||||
def try_parse_json(payload):
|
||||
"""尝试将 payload 解析为 JSON。dc_data 是二进制帧中嵌入的 JSON"""
|
||||
if payload is None:
|
||||
return None
|
||||
try:
|
||||
text = payload.decode('utf-8')
|
||||
return json.loads(text)
|
||||
except:
|
||||
return None
|
||||
|
||||
def drain_sock(sock, timeout=1.0):
|
||||
"""排空 socket 缓冲区中的所有待处理帧,返回所有解析后的消息"""
|
||||
msgs = []
|
||||
while True:
|
||||
opcode, payload = ws_recv(sock, timeout=timeout)
|
||||
if opcode is None:
|
||||
break
|
||||
# 文本帧
|
||||
if opcode == 0x01 and payload:
|
||||
obj = try_parse_json(payload)
|
||||
if obj:
|
||||
msgs.append(('text', obj))
|
||||
else:
|
||||
msgs.append(('text', payload.decode('utf-8', errors='replace')))
|
||||
# 二进制帧(dc_data 在这里)
|
||||
elif opcode == 0x02 and payload:
|
||||
obj = try_parse_json(payload)
|
||||
if obj:
|
||||
msgs.append(('binary', obj))
|
||||
else:
|
||||
msgs.append(('binary', payload))
|
||||
elif opcode == 0x08:
|
||||
msgs.append(('close', None))
|
||||
break
|
||||
elif opcode == 0x09: # ping
|
||||
pass
|
||||
return msgs
|
||||
|
||||
|
||||
# =========================== 测试主流程 ===========================
|
||||
|
||||
def main():
|
||||
print("=" * 70)
|
||||
print(" RTU 数据中心 → 信号管理页(out) 添加信号功能测试")
|
||||
print(f" 目标: {IP}:{PORT}")
|
||||
print("=" * 70)
|
||||
|
||||
# ---- 连接 ----
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(10)
|
||||
try:
|
||||
sock.connect((IP, PORT))
|
||||
print(f"[✓] TCP 连接成功 → {IP}:{PORT}")
|
||||
except Exception as e:
|
||||
print(f"[✗] TCP 连接失败: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
ws_handshake(sock)
|
||||
|
||||
# 原始 socket 级别检查是否有数据
|
||||
print("[i] 检查原始 socket 是否有数据到达...")
|
||||
sock.settimeout(3.0)
|
||||
try:
|
||||
raw_data = sock.recv(65536)
|
||||
if raw_data:
|
||||
print(f"[i] 收到原始数据: {len(raw_data)} 字节")
|
||||
print(f" 前 50 字节(hex): {raw_data[:50].hex()}")
|
||||
print(f" 前 200 字节(str): {raw_data[:200]}")
|
||||
# 尝试解析 WebSocket 帧
|
||||
if len(raw_data) >= 2:
|
||||
b0, b1 = raw_data[0], raw_data[1]
|
||||
opcode = b0 & 0x0F
|
||||
fin = (b0 >> 7) & 1
|
||||
print(f" FIN={fin} opcode={opcode} ({'text' if opcode==1 else 'binary' if opcode==2 else 'close' if opcode==8 else 'other'})")
|
||||
else:
|
||||
print("[i] 原始 socket 无数据")
|
||||
except socket.timeout:
|
||||
print("[i] 原始 socket 超时,无数据")
|
||||
sock.settimeout(10)
|
||||
|
||||
# 等待初始推送 (cmd_list 等)
|
||||
time.sleep(1.0)
|
||||
msgs = drain_sock(sock, timeout=2.0)
|
||||
print(f"[i] 初始消息数: {len(msgs)}")
|
||||
for kind, msg in msgs:
|
||||
if isinstance(msg, dict):
|
||||
print(f" [{kind}] type={msg.get('type','?')}, keys={list(msg.keys())}")
|
||||
if msg.get('type') == 'cmd_list':
|
||||
print(f" [i] 收到 cmd_list,命令数: {len(msg.get('cmds', []))}")
|
||||
elif isinstance(msg, str):
|
||||
print(f" [{kind}] str: {msg[:200]}")
|
||||
elif isinstance(msg, bytes):
|
||||
print(f" [{kind}] bytes: {len(msg)}B, 前200字节: {msg[:200]}")
|
||||
|
||||
# ---- 步骤 1: 通过终端命令注册 dc_data 信号 ----
|
||||
print("\n" + "=" * 70)
|
||||
print(" 步骤 1: 通过 datacenter 命令注册所有类型的信号")
|
||||
print("=" * 70)
|
||||
|
||||
types_cn = {'out': '注册', 'in': '链接注册', 'yk': '遥控', 'ao': '参数', 'param': '定值'}
|
||||
all_dc_data = {}
|
||||
|
||||
for st in ['out', 'in', 'yk', 'ao', 'param']:
|
||||
print(f"\n [→] 执行: datacenter {st}")
|
||||
ws_send(sock, {"type": "cmd", "cmd": f"datacenter {st}"})
|
||||
time.sleep(2.0) # 等待更长时间
|
||||
|
||||
# dc_data 通过二进制帧返回
|
||||
msgs = drain_sock(sock, timeout=3.0)
|
||||
print(f" [i] 收到 {len(msgs)} 条消息")
|
||||
for kind, msg in msgs:
|
||||
if isinstance(msg, dict):
|
||||
print(f" [{kind}] type={msg.get('type','?')}")
|
||||
if msg.get('type') == 'dc_data':
|
||||
all_dc_data[st] = msg
|
||||
for sig_type in ['out', 'in', 'yk', 'ao', 'param']:
|
||||
count = len(msg.get(sig_type, []))
|
||||
if count > 0:
|
||||
print(f" {sig_type}: {count} 个信号")
|
||||
for k in msg:
|
||||
if k != 'type' and isinstance(msg[k], list):
|
||||
print(f" {k}: {len(msg[k])} 个信号")
|
||||
for item in msg[k][:3]:
|
||||
print(f" - saddr={item.get('saddr','?')} desc={item.get('desc','?')}")
|
||||
elif isinstance(msg, str):
|
||||
print(f" [{kind}] str({len(msg)}): {msg[:300]}")
|
||||
elif isinstance(msg, bytes):
|
||||
print(f" [{kind}] bytes({len(msg)}): {msg[:200]}")
|
||||
|
||||
# ---- 步骤 2: 检查 dc_data 并选择 out 信号 ----
|
||||
print("\n" + "=" * 70)
|
||||
print(" 步骤 2: 从 dc_data 选择 out 信号添加到配置页")
|
||||
print("=" * 70)
|
||||
|
||||
dc_out = all_dc_data.get('out', {})
|
||||
out_signals = dc_out.get('out', [])
|
||||
|
||||
if not out_signals:
|
||||
print(" [!] 没有可用的 out 信号!")
|
||||
print(" 可能原因: 服务端 datacenter out 未注册任何信号")
|
||||
sock.close()
|
||||
return
|
||||
|
||||
# 选择前 3 个 out 信号(只选择直控或选控类型用于测试)
|
||||
test_signals = [s for s in out_signals if s.get('ctrl_type', 0) in [1, 2]]
|
||||
if not test_signals:
|
||||
test_signals = out_signals[:3] # 降级:选前 3 个
|
||||
test_signals = test_signals[:3]
|
||||
|
||||
print(f" 从 {len(out_signals)} 个 out 信号中选择 {len(test_signals)} 个测试:")
|
||||
for i, sig in enumerate(test_signals):
|
||||
saddr = sig.get('saddr', '')
|
||||
print(f" [{i+1}] saddr={saddr} desc={sig.get('desc','?')} ctrl_type={sig.get('ctrl_type','?')}")
|
||||
|
||||
# 先排空已有消息
|
||||
drain_sock(sock, timeout=0.5)
|
||||
|
||||
# ---- 步骤 3: 执行 curd:add 添加信号 ----
|
||||
print("\n" + "=" * 70)
|
||||
print(" 步骤 3: 发送 curd:add 添加信号到配置页(out)")
|
||||
print("=" * 70)
|
||||
|
||||
added_saddrs = set()
|
||||
for i, sig in enumerate(test_signals):
|
||||
saddr = sig.get('saddr', '')
|
||||
add_msg = {
|
||||
"saddr": saddr,
|
||||
"signal_type": "out",
|
||||
"curd": "add",
|
||||
"setting_zone": "0",
|
||||
"signal_data": ""
|
||||
}
|
||||
print(f"\n [{i+1}] 添加: saddr={saddr}")
|
||||
ws_send(sock, add_msg)
|
||||
added_saddrs.add(saddr)
|
||||
time.sleep(2.0) # 2 秒间隔
|
||||
|
||||
# ---- 步骤 4: 等待 ws_task 推送 out 数据 ----
|
||||
print("\n" + "=" * 70)
|
||||
print(" 步骤 4: 等待 ws_task 定时推送 out 数据(最多 10 秒)")
|
||||
print("=" * 70)
|
||||
|
||||
out_push_received = False
|
||||
out_signals_list = []
|
||||
start = time.time()
|
||||
while time.time() - start < 10.0:
|
||||
msgs = drain_sock(sock, timeout=2.0)
|
||||
for kind, msg in msgs:
|
||||
if isinstance(msg, dict):
|
||||
out_data = msg.get('out')
|
||||
if out_data is not None and isinstance(out_data, list) and len(out_data) > 0:
|
||||
out_push_received = True
|
||||
out_signals_list = out_data
|
||||
elapsed = time.time() - start
|
||||
print(f"\n [✓] 收到 out 数据推送! (耗时 {elapsed:.1f}s, {kind}帧)")
|
||||
print(f" out 当前注册信号数: {len(out_data)}")
|
||||
for j, o in enumerate(out_data):
|
||||
marker = " ← 新添加" if o.get('saddr') in added_saddrs else ""
|
||||
print(f" [{j+1}] saddr={o.get('saddr','?')} val={o.get('val','?')} desc={o.get('desc','?')}{marker}")
|
||||
break
|
||||
elif msg.get('type') == 'dc_data':
|
||||
pass # 忽略 dc_data 推送
|
||||
elif msg.get('type'):
|
||||
pass
|
||||
elif isinstance(msg, str) and len(msg) > 5:
|
||||
pass # 可能是 cmd 输出
|
||||
if out_push_received:
|
||||
break
|
||||
|
||||
# ---- 步骤 5: 验证结果 ----
|
||||
print("\n" + "=" * 70)
|
||||
print(" 步骤 5: 验证结果")
|
||||
print("=" * 70)
|
||||
|
||||
if not out_push_received:
|
||||
print(" [✗] 未收到 out 数据推送")
|
||||
print(" 可能原因:")
|
||||
print(" 1. ws_task 定时器未触发(等待时间不足)")
|
||||
print(" 2. add_signal 失败(saddr 无效或信号已存在)")
|
||||
print(" 3. 服务端没有 has_change(新信号初始值与实际值相同)")
|
||||
else:
|
||||
registered_saddrs = {o.get('saddr') for o in out_signals_list}
|
||||
all_found = added_saddrs.issubset(registered_saddrs)
|
||||
missing = added_saddrs - registered_saddrs
|
||||
|
||||
if all_found:
|
||||
print(f" [✓] 所有 {len(added_saddrs)} 个测试信号已成功注册到 out!")
|
||||
else:
|
||||
print(f" [✗] 以下 {len(missing)} 个信号未能在 out 中找到: {missing}")
|
||||
|
||||
if missing:
|
||||
print(f"\n 当前 out 中注册的信号 saddr: {registered_saddrs}")
|
||||
|
||||
# ---- 步骤 6: 清理 — 删除测试添加的信号 ----
|
||||
print("\n" + "=" * 70)
|
||||
print(" 步骤 6: 清理 — 删除测试添加的信号")
|
||||
print("=" * 70)
|
||||
|
||||
for sig in test_signals:
|
||||
saddr = sig.get('saddr', '')
|
||||
del_msg = {
|
||||
"saddr": saddr,
|
||||
"signal_type": "out",
|
||||
"curd": "del",
|
||||
"setting_zone": "0",
|
||||
"signal_data": ""
|
||||
}
|
||||
print(f" [→] 删除: saddr={saddr}")
|
||||
ws_send(sock, del_msg)
|
||||
time.sleep(0.5)
|
||||
# del 后立即推送,检查删除结果
|
||||
msgs = drain_sock(sock, timeout=2.0)
|
||||
for kind, msg in msgs:
|
||||
if isinstance(msg, dict) and msg.get('out') is not None:
|
||||
print(f" [i] 删除后 out 信号数: {len(msg.get('out', []))}")
|
||||
|
||||
# ---- 总结 ----
|
||||
print("\n" + "=" * 70)
|
||||
print(" 测试总结")
|
||||
print("=" * 70)
|
||||
print(f" 数据中心 out 信号数: {len(out_signals)}")
|
||||
print(f" 测试添加信号数: {len(test_signals)}")
|
||||
if out_push_received:
|
||||
print(f" out 推送接收: ✅")
|
||||
print(f" 信号注册成功: {'✅ 全部通过' if all_found else f'❌ 缺少 {len(missing)} 个'}")
|
||||
else:
|
||||
print(f" out 推送接收: ❌ 未收到")
|
||||
print("=" * 70)
|
||||
|
||||
# 发送关闭帧
|
||||
close_frame = struct.pack('>H', 1000)
|
||||
frame = bytearray([0x88, 0x02])
|
||||
frame.extend(close_frame)
|
||||
try:
|
||||
sock.sendall(bytes(frame))
|
||||
except:
|
||||
pass
|
||||
sock.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
@ -0,0 +1,258 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
数据中心 → 信号管理页面 (out) 添加信号功能测试 (v2)
|
||||
使用原始帧接收方式(已验证可用)
|
||||
"""
|
||||
import socket, base64, os, struct, json, time, sys
|
||||
|
||||
IP = '198.120.0.100'
|
||||
PORT = 8000
|
||||
|
||||
def ws_handshake(sock):
|
||||
key = base64.b64encode(os.urandom(16)).decode()
|
||||
req = (f"GET /ws HTTP/1.1\r\nHost: {IP}:{PORT}\r\nUpgrade: websocket\r\n"
|
||||
f"Connection: Upgrade\r\nSec-WebSocket-Key: {key}\r\n"
|
||||
f"Sec-WebSocket-Version: 13\r\n\r\n")
|
||||
sock.sendall(req.encode())
|
||||
resp = b""
|
||||
while b"\r\n\r\n" not in resp:
|
||||
c = sock.recv(4096)
|
||||
if not c: break
|
||||
resp += c
|
||||
if b"101" not in resp.split(b"\r\n")[0]:
|
||||
raise Exception(f"Handshake failed: {resp[:200]}")
|
||||
return resp
|
||||
|
||||
def ws_send_text(sock, msg):
|
||||
"""发送文本帧"""
|
||||
if isinstance(msg, dict):
|
||||
msg = json.dumps(msg, ensure_ascii=False)
|
||||
payload = msg.encode()
|
||||
L = len(payload)
|
||||
mask = os.urandom(4)
|
||||
masked = bytes(payload[i] ^ mask[i % 4] for i in range(L))
|
||||
frame = bytearray([0x81])
|
||||
if L < 126:
|
||||
frame.append(0x80 | L)
|
||||
elif L < 65536:
|
||||
frame.append(0x80 | 126)
|
||||
frame.extend(struct.pack('>H', L))
|
||||
else:
|
||||
frame.append(0x80 | 127)
|
||||
frame.extend(struct.pack('>Q', L))
|
||||
frame.extend(mask)
|
||||
frame.extend(masked)
|
||||
sock.sendall(bytes(frame))
|
||||
|
||||
def ws_recv_all(sock, timeout=2.0):
|
||||
"""接收所有可用的 WebSocket 帧,解析后返回消息列表"""
|
||||
sock.settimeout(timeout)
|
||||
all_data = b""
|
||||
try:
|
||||
while True:
|
||||
chunk = sock.recv(65536)
|
||||
if not chunk:
|
||||
break
|
||||
all_data += chunk
|
||||
except socket.timeout:
|
||||
pass
|
||||
|
||||
msgs = []
|
||||
offset = 0
|
||||
while offset + 2 <= len(all_data):
|
||||
b0 = all_data[offset]
|
||||
b1 = all_data[offset + 1]
|
||||
opcode = b0 & 0x0F
|
||||
length = b1 & 0x7F
|
||||
offset += 2
|
||||
if length == 126:
|
||||
if offset + 2 > len(all_data): break
|
||||
length = struct.unpack('>H', all_data[offset:offset+2])[0]
|
||||
offset += 2
|
||||
elif length == 127:
|
||||
if offset + 8 > len(all_data): break
|
||||
length = struct.unpack('>Q', all_data[offset:offset+8])[0]
|
||||
offset += 8
|
||||
if offset + length > len(all_data):
|
||||
break
|
||||
payload = all_data[offset:offset+length]
|
||||
offset += length
|
||||
|
||||
if opcode == 0x01: # text
|
||||
try:
|
||||
msgs.append(json.loads(payload.decode('utf-8')))
|
||||
except:
|
||||
msgs.append(payload.decode('utf-8', errors='replace'))
|
||||
elif opcode == 0x02: # binary (dc_data 在这里)
|
||||
try:
|
||||
msgs.append(json.loads(payload.decode('utf-8')))
|
||||
except:
|
||||
msgs.append(payload)
|
||||
elif opcode == 0x08: # close
|
||||
pass
|
||||
return msgs
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 70)
|
||||
print(" RTU 数据中心 → 信号管理页(out) 添加信号功能测试")
|
||||
print(f" 目标: {IP}:{PORT}")
|
||||
print("=" * 70)
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(10)
|
||||
sock.connect((IP, PORT))
|
||||
print(f"[✓] TCP 连接成功")
|
||||
|
||||
ws_handshake(sock)
|
||||
print(f"[✓] WebSocket 握手成功")
|
||||
|
||||
# ---- 0. 初始握手:获取 cmd_list ----
|
||||
ws_send_text(sock, {"type": "get_cmds"})
|
||||
time.sleep(0.5)
|
||||
msgs = ws_recv_all(sock, timeout=2.0)
|
||||
for msg in msgs:
|
||||
if isinstance(msg, dict) and msg.get('type') == 'cmd_list':
|
||||
print(f"[✓] cmd_list: {len(msg.get('cmds', []))} 个命令")
|
||||
for cmd in msg.get('cmds', []):
|
||||
print(f" - {cmd.get('name')}: {cmd.get('desc')}")
|
||||
|
||||
# ---- 1. 注册 dc_data 信号 ----
|
||||
print("\n" + "=" * 70)
|
||||
print(" 步骤 1: 通过 datacenter 命令注册所有信号")
|
||||
print("=" * 70)
|
||||
|
||||
all_dc_data = {}
|
||||
for st in ['out', 'in', 'yk', 'ao', 'param']:
|
||||
print(f"\n [→] datacenter {st}")
|
||||
ws_send_text(sock, {"type": "cmd", "cmd": f"datacenter {st}"})
|
||||
time.sleep(1.5)
|
||||
msgs = ws_recv_all(sock, timeout=3.0)
|
||||
for msg in msgs:
|
||||
if isinstance(msg, dict):
|
||||
if msg.get('type') == 'dc_data':
|
||||
all_dc_data[st] = msg
|
||||
cnt = len(msg.get(st, []))
|
||||
print(f" [✓] dc_data: {st} = {cnt} 个信号")
|
||||
if cnt > 0:
|
||||
for item in msg.get(st, [])[:5]:
|
||||
print(f" saddr={item.get('saddr','?')} desc={item.get('desc','?')} ctrl={item.get('ctrl_type','?')}")
|
||||
if cnt > 5:
|
||||
print(f" ... 还有 {cnt-5} 个")
|
||||
elif msg.get('type'):
|
||||
pass # 忽略
|
||||
elif isinstance(msg, str) and len(msg) < 300:
|
||||
print(f" [cmd输出] {msg.strip()[:200]}")
|
||||
|
||||
# ---- 2. 选择 out 信号 ----
|
||||
print("\n" + "=" * 70)
|
||||
print(" 步骤 2: 选择 out 信号并添加到配置页")
|
||||
print("=" * 70)
|
||||
|
||||
dc_out = all_dc_data.get('out', {})
|
||||
out_signals = dc_out.get('out', [])
|
||||
|
||||
if not out_signals:
|
||||
print(" [!] 没有可用的 out 信号!")
|
||||
print(" 尝试直接添加一个已知信号测试...")
|
||||
# 尝试添加一个可能存在的信号
|
||||
test_saddr = "iec.self_check"
|
||||
print(f" [→] 尝试添加: {test_saddr}")
|
||||
ws_send_text(sock, {"saddr": test_saddr, "signal_type": "out", "curd": "add", "setting_zone": "0", "signal_data": ""})
|
||||
time.sleep(3.0)
|
||||
msgs = ws_recv_all(sock, timeout=3.0)
|
||||
print(f" 收到 {len(msgs)} 条消息")
|
||||
for msg in msgs:
|
||||
if isinstance(msg, dict):
|
||||
print(f" type={msg.get('type','?')}")
|
||||
out_data = msg.get('out')
|
||||
if out_data is not None:
|
||||
print(f" out 信号数: {len(out_data) if isinstance(out_data, list) else '?'}")
|
||||
if isinstance(out_data, list):
|
||||
for o in out_data:
|
||||
print(f" saddr={o.get('saddr','?')} val={o.get('val','?')}")
|
||||
else:
|
||||
# 选择前3个信号
|
||||
test_signals = out_signals[:3]
|
||||
test_saddrs = {s.get('saddr') for s in test_signals}
|
||||
print(f" 从 {len(out_signals)} 个信号中选择 {len(test_signals)} 个:")
|
||||
for i, sig in enumerate(test_signals):
|
||||
print(f" [{i+1}] saddr={sig.get('saddr')} desc={sig.get('desc','?')}")
|
||||
|
||||
# ---- 3. 执行 curd:add ----
|
||||
print("\n" + "=" * 70)
|
||||
print(" 步骤 3: 执行 curd:add")
|
||||
print("=" * 70)
|
||||
|
||||
for i, sig in enumerate(test_signals):
|
||||
saddr = sig.get('saddr')
|
||||
print(f"\n [{i+1}] curd:add saddr={saddr}")
|
||||
ws_send_text(sock, {"saddr": saddr, "signal_type": "out", "curd": "add", "setting_zone": "0", "signal_data": ""})
|
||||
time.sleep(2.0)
|
||||
|
||||
# ---- 4. 等待 ws_task 推送 out 数据 ----
|
||||
print("\n" + "=" * 70)
|
||||
print(" 步骤 4: 等待 ws_task 推送 out 数据(最多15秒)")
|
||||
print("=" * 70)
|
||||
|
||||
out_received = False
|
||||
out_data = []
|
||||
start = time.time()
|
||||
while time.time() - start < 15.0:
|
||||
msgs = ws_recv_all(sock, timeout=3.0)
|
||||
for msg in msgs:
|
||||
if isinstance(msg, dict):
|
||||
out_arr = msg.get('out')
|
||||
if isinstance(out_arr, list) and len(out_arr) > 0:
|
||||
out_data = out_arr
|
||||
out_received = True
|
||||
break
|
||||
if out_received:
|
||||
break
|
||||
if msgs:
|
||||
print(f" [i] 等待中... ({time.time()-start:.0f}s)")
|
||||
|
||||
# ---- 5. 验证 ----
|
||||
print("\n" + "=" * 70)
|
||||
print(" 步骤 5: 验证结果")
|
||||
print("=" * 70)
|
||||
|
||||
if out_received:
|
||||
print(f" [✓] 收到 out 数据: {len(out_data)} 个信号")
|
||||
registered = {o.get('saddr') for o in out_data}
|
||||
for o in out_data:
|
||||
marker = " ← 新添加" if o.get('saddr') in test_saddrs else ""
|
||||
print(f" saddr={o.get('saddr','?')} val={o.get('val','?')} desc={o.get('desc','?')}{marker}")
|
||||
|
||||
all_found = test_saddrs.issubset(registered)
|
||||
missing = test_saddrs - registered
|
||||
if all_found:
|
||||
print(f"\n [✓] 所有 {len(test_saddrs)} 个信号已成功注册到 out!")
|
||||
else:
|
||||
print(f"\n [✗] 缺少 {len(missing)} 个: {missing}")
|
||||
print(f" 当前已注册: {registered}")
|
||||
|
||||
# ---- 6. 清理 ----
|
||||
print("\n" + "=" * 70)
|
||||
print(" 步骤 6: 清理 — 删除测试信号")
|
||||
print("=" * 70)
|
||||
for sig in test_signals:
|
||||
saddr = sig.get('saddr')
|
||||
print(f" [→] del saddr={saddr}")
|
||||
ws_send_text(sock, {"saddr": saddr, "signal_type": "out", "curd": "del", "setting_zone": "0", "signal_data": ""})
|
||||
time.sleep(0.3)
|
||||
else:
|
||||
print(" [✗] 未收到 out 数据推送")
|
||||
print(" 可能原因: add_signal 失败,或 ws_task 未推送变化")
|
||||
|
||||
# 清理 - 发送关闭帧
|
||||
try:
|
||||
sock.sendall(b'\x88\x80' + os.urandom(4)) # close frame
|
||||
except:
|
||||
pass
|
||||
sock.close()
|
||||
print("\n[✓] 测试完成")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
@ -0,0 +1,245 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
数据中心 → 信号管理页面 (out) 添加信号功能测试 (v3)
|
||||
修复: 终端命令用 "data" 字段而非 "cmd"
|
||||
"""
|
||||
import socket, base64, os, struct, json, time, sys
|
||||
|
||||
IP = '198.120.0.100'; PORT = 8000
|
||||
|
||||
def ws_handshake(sock):
|
||||
key = base64.b64encode(os.urandom(16)).decode()
|
||||
req = (f"GET /ws HTTP/1.1\r\nHost: {IP}:{PORT}\r\nUpgrade: websocket\r\n"
|
||||
f"Connection: Upgrade\r\nSec-WebSocket-Key: {key}\r\n"
|
||||
f"Sec-WebSocket-Version: 13\r\n\r\n")
|
||||
sock.sendall(req.encode())
|
||||
resp = b""
|
||||
while b"\r\n\r\n" not in resp:
|
||||
c = sock.recv(4096)
|
||||
if not c: break
|
||||
resp += c
|
||||
if b"101" not in resp.split(b"\r\n")[0]:
|
||||
raise Exception(f"Handshake failed: {resp[:200]}")
|
||||
return resp
|
||||
|
||||
def ws_send(sock, msg):
|
||||
if isinstance(msg, dict):
|
||||
msg = json.dumps(msg, ensure_ascii=False)
|
||||
payload = msg.encode()
|
||||
L = len(payload)
|
||||
mask = os.urandom(4)
|
||||
masked = bytes(payload[i] ^ mask[i % 4] for i in range(L))
|
||||
frame = bytearray([0x81])
|
||||
if L < 126:
|
||||
frame.append(0x80 | L)
|
||||
elif L < 65536:
|
||||
frame.append(0x80 | 126)
|
||||
frame.extend(struct.pack('>H', L))
|
||||
else:
|
||||
frame.append(0x80 | 127)
|
||||
frame.extend(struct.pack('>Q', L))
|
||||
frame.extend(mask)
|
||||
frame.extend(masked)
|
||||
sock.sendall(bytes(frame))
|
||||
|
||||
def ws_recv_frames(sock, timeout=2.0):
|
||||
"""接收所有可用帧,解析后返回消息列表"""
|
||||
sock.settimeout(timeout)
|
||||
all_data = b""
|
||||
try:
|
||||
while True:
|
||||
chunk = sock.recv(65536)
|
||||
if not chunk: break
|
||||
all_data += chunk
|
||||
except socket.timeout:
|
||||
pass
|
||||
|
||||
msgs = []
|
||||
offset = 0
|
||||
while offset + 2 <= len(all_data):
|
||||
b0, b1 = all_data[offset], all_data[offset + 1]
|
||||
opcode = b0 & 0x0F
|
||||
length = b1 & 0x7F
|
||||
offset += 2
|
||||
if length == 126:
|
||||
if offset + 2 > len(all_data): break
|
||||
length = struct.unpack('>H', all_data[offset:offset+2])[0]
|
||||
offset += 2
|
||||
elif length == 127:
|
||||
if offset + 8 > len(all_data): break
|
||||
length = struct.unpack('>Q', all_data[offset:offset+8])[0]
|
||||
offset += 8
|
||||
if offset + length > len(all_data):
|
||||
break
|
||||
payload = all_data[offset:offset+length]
|
||||
offset += length
|
||||
|
||||
if opcode in (0x01, 0x02):
|
||||
try:
|
||||
msgs.append(json.loads(payload.decode('utf-8')))
|
||||
except:
|
||||
msgs.append(payload.decode('utf-8', errors='replace'))
|
||||
elif opcode == 0x08:
|
||||
pass
|
||||
return msgs
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 70)
|
||||
print(" RTU 数据中心 → 信号管理页(out) 添加信号功能测试")
|
||||
print(f" 目标: {IP}:{PORT}")
|
||||
print("=" * 70)
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(10)
|
||||
sock.connect((IP, PORT))
|
||||
print(f"[✓] TCP 连接成功")
|
||||
|
||||
ws_handshake(sock)
|
||||
print("[✓] WebSocket 握手成功")
|
||||
|
||||
# ---- 步骤 1: 注册 dc_data 信号 ----
|
||||
# 注意: 终端命令格式为 {"type":"cmd","data":"datacenter out"}
|
||||
print("\n" + "=" * 70)
|
||||
print(" 步骤 1: 通过 datacenter 命令注册所有类型信号")
|
||||
print("=" * 70)
|
||||
|
||||
all_dc_data = {}
|
||||
for st in ['out', 'in', 'yk', 'ao', 'param']:
|
||||
print(f"\n [→] datacenter {st}")
|
||||
ws_send(sock, {"type": "cmd", "data": f"datacenter {st}"})
|
||||
time.sleep(1.5)
|
||||
msgs = ws_recv_frames(sock, timeout=3.0)
|
||||
print(f" [i] 收到 {len(msgs)} 条消息")
|
||||
for msg in msgs:
|
||||
if isinstance(msg, dict):
|
||||
if msg.get('type') == 'dc_data':
|
||||
all_dc_data[st] = msg
|
||||
cnt = len(msg.get(st, []))
|
||||
print(f" [✓] dc_data: {st} = {cnt} 个信号")
|
||||
for item in msg.get(st, [])[:5]:
|
||||
print(f" saddr={item.get('saddr','?')} desc={item.get('desc','?')} ctrl={item.get('ctrl_type','?')}")
|
||||
if cnt > 5:
|
||||
print(f" ... 还有 {cnt-5} 个")
|
||||
elif isinstance(msg, str) and len(msg) < 500:
|
||||
print(f" [cmd输出] {msg.strip()[:200]}")
|
||||
|
||||
# ---- 步骤 2: 选择 out 信号 ----
|
||||
print("\n" + "=" * 70)
|
||||
print(" 步骤 2: 选择 out 信号并添加到配置页")
|
||||
print("=" * 70)
|
||||
|
||||
dc_out = all_dc_data.get('out', {})
|
||||
out_signals = dc_out.get('out', [])
|
||||
|
||||
if not out_signals:
|
||||
print(" [!] 没有可用的 out 信号")
|
||||
print(" 原因: datacenter 命令未注册任何 out 信号到数据中心")
|
||||
print(" 可能需要在配置文件中添加信号定义")
|
||||
sock.close()
|
||||
return
|
||||
|
||||
test_signals = out_signals[:3]
|
||||
test_saddrs = {s.get('saddr') for s in test_signals}
|
||||
print(f" 从 {len(out_signals)} 个信号中选择 {len(test_signals)} 个:")
|
||||
for i, sig in enumerate(test_signals):
|
||||
print(f" [{i+1}] saddr={sig.get('saddr')} desc={sig.get('desc','?')}")
|
||||
|
||||
# 排空之前的消息
|
||||
ws_recv_frames(sock, timeout=0.5)
|
||||
|
||||
# ---- 步骤 3: 执行 curd:add ----
|
||||
print("\n" + "=" * 70)
|
||||
print(" 步骤 3: 执行 curd:add (模拟前端「添加到配置页」)")
|
||||
print("=" * 70)
|
||||
|
||||
for i, sig in enumerate(test_signals):
|
||||
saddr = sig.get('saddr')
|
||||
print(f"\n [{i+1}] curd:add saddr={saddr}")
|
||||
ws_send(sock, {
|
||||
"saddr": saddr,
|
||||
"signal_type": "out",
|
||||
"curd": "add",
|
||||
"setting_zone": "0",
|
||||
"signal_data": ""
|
||||
})
|
||||
time.sleep(2.0) # 2秒间隔模拟真人操作
|
||||
|
||||
# ---- 步骤 4: 等待 ws_task 推送 out 数据 ----
|
||||
print("\n" + "=" * 70)
|
||||
print(" 步骤 4: 等待 ws_task 周期性推送 out 数据")
|
||||
print(" (add 操作不会立即推送,需要等 ws_task 检测到 has_change)")
|
||||
print("=" * 70)
|
||||
|
||||
out_received = False
|
||||
out_data = []
|
||||
start = time.time()
|
||||
while time.time() - start < 15.0:
|
||||
msgs = ws_recv_frames(sock, timeout=3.0)
|
||||
for msg in msgs:
|
||||
if isinstance(msg, dict):
|
||||
out_arr = msg.get('out')
|
||||
if isinstance(out_arr, list) and len(out_arr) > 0:
|
||||
out_data = out_arr
|
||||
out_received = True
|
||||
break
|
||||
if out_received:
|
||||
break
|
||||
elapsed = time.time() - start
|
||||
if elapsed > 2:
|
||||
print(f" [i] 等待中... ({elapsed:.0f}s)")
|
||||
|
||||
# ---- 步骤 5: 验证结果 ----
|
||||
print("\n" + "=" * 70)
|
||||
print(" 步骤 5: 验证结果")
|
||||
print("=" * 70)
|
||||
|
||||
if out_received:
|
||||
print(f" [✓] 收到 out 数据推送: {len(out_data)} 个注册信号")
|
||||
registered_saddrs = {o.get('saddr') for o in out_data}
|
||||
for o in out_data:
|
||||
marker = " ← 新添加" if o.get('saddr') in test_saddrs else ""
|
||||
print(f" saddr={o.get('saddr','?')} val={o.get('val','?')} desc={o.get('desc','?')}{marker}")
|
||||
|
||||
all_found = test_saddrs.issubset(registered_saddrs)
|
||||
missing = test_saddrs - registered_saddrs
|
||||
|
||||
if all_found:
|
||||
print(f"\n ✅ 所有 {len(test_saddrs)} 个信号成功注册到 out!")
|
||||
else:
|
||||
print(f"\n ❌ 缺少 {len(missing)} 个信号: {missing}")
|
||||
else:
|
||||
print(" ❌ 未收到 out 数据推送")
|
||||
print(" 说明: add_signal 后服务端没有通过 ws_task 推送变化")
|
||||
print(" 可能原因: has_change 为 false (新信号初始值可能与 last_val 相同)")
|
||||
|
||||
# ---- 步骤 6: 清理 ----
|
||||
print("\n" + "=" * 70)
|
||||
print(" 步骤 6: 清理 — 删除测试添加的信号")
|
||||
print("=" * 70)
|
||||
|
||||
for sig in test_signals:
|
||||
saddr = sig.get('saddr')
|
||||
print(f" [→] del saddr={saddr}")
|
||||
ws_send(sock, {
|
||||
"saddr": saddr, "signal_type": "out",
|
||||
"curd": "del", "setting_zone": "0", "signal_data": ""
|
||||
})
|
||||
time.sleep(0.3)
|
||||
# del 后立即推送
|
||||
msgs = ws_recv_frames(sock, timeout=2.0)
|
||||
for msg in msgs:
|
||||
if isinstance(msg, dict) and msg.get('out') is not None:
|
||||
print(f" [i] 删除后 out 信号数: {len(msg['out'])}")
|
||||
|
||||
# 发送关闭帧
|
||||
try:
|
||||
sock.sendall(b'\x88\x80' + os.urandom(4))
|
||||
except:
|
||||
pass
|
||||
sock.close()
|
||||
print("\n[✓] 测试完成")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
#!/usr/bin/env python3
|
||||
"""最简 WebSocket 测试 - 验证基本通信"""
|
||||
import socket, base64, os, struct, json, time, sys
|
||||
|
||||
IP = '198.120.0.100'
|
||||
PORT = 8000
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(10)
|
||||
sock.connect((IP, PORT))
|
||||
print(f"[✓] TCP 连接成功")
|
||||
|
||||
# WebSocket 握手
|
||||
key = base64.b64encode(os.urandom(16)).decode()
|
||||
req = (f"GET /ws HTTP/1.1\r\nHost: {IP}:{PORT}\r\nUpgrade: websocket\r\n"
|
||||
f"Connection: Upgrade\r\nSec-WebSocket-Key: {key}\r\n"
|
||||
f"Sec-WebSocket-Version: 13\r\n\r\n")
|
||||
sock.sendall(req.encode())
|
||||
|
||||
# 接收 HTTP 响应
|
||||
resp = b""
|
||||
while b"\r\n\r\n" not in resp:
|
||||
chunk = sock.recv(4096)
|
||||
if not chunk: break
|
||||
resp += chunk
|
||||
print(f"[✓] HTTP 响应: {resp.split(b'\r\n')[0].decode()}")
|
||||
|
||||
# 检查响应码
|
||||
if b"101" not in resp.split(b"\r\n")[0]:
|
||||
print(f"[✗] 升级失败: {resp.decode(errors='replace')[:500]}")
|
||||
sys.exit(1)
|
||||
|
||||
# 检查是否有额外的数据(WebSocket 帧紧跟在 HTTP 响应后)
|
||||
headers_end = resp.find(b"\r\n\r\n") + 4
|
||||
extra = resp[headers_end:]
|
||||
if extra:
|
||||
print(f"[i] HTTP 响应后有额外 {len(extra)} 字节: {extra.hex()}")
|
||||
|
||||
# 手动构造 WebSocket 文本帧发送 get_cmds
|
||||
msg = json.dumps({"type": "get_cmds"})
|
||||
payload = msg.encode()
|
||||
L = len(payload)
|
||||
mask = os.urandom(4)
|
||||
masked = bytes(payload[i] ^ mask[i % 4] for i in range(L))
|
||||
|
||||
frame = bytearray([0x81]) # FIN + text
|
||||
if L < 126:
|
||||
frame.append(0x80 | L)
|
||||
else:
|
||||
frame.append(0x80 | 126)
|
||||
frame.extend(struct.pack('>H', L))
|
||||
frame.extend(mask)
|
||||
frame.extend(masked)
|
||||
sock.sendall(bytes(frame))
|
||||
print(f"[→] 发送 get_cmds ({L} 字节)")
|
||||
|
||||
# 等待接收
|
||||
sock.settimeout(3.0)
|
||||
all_data = b""
|
||||
try:
|
||||
while True:
|
||||
chunk = sock.recv(65536)
|
||||
if not chunk:
|
||||
break
|
||||
all_data += chunk
|
||||
print(f"[i] recv chunk: {len(chunk)} 字节")
|
||||
except socket.timeout:
|
||||
print(f"[i] recv 超时")
|
||||
|
||||
print(f"\n总共接收到: {len(all_data)} 字节")
|
||||
|
||||
if len(all_data) == 0:
|
||||
print("[✗] 未收到任何数据!服务器可能未响应")
|
||||
sys.exit(1)
|
||||
|
||||
# 解析 WebSocket 帧
|
||||
offset = 0
|
||||
while offset < len(all_data):
|
||||
if offset + 2 > len(all_data): break
|
||||
b0 = all_data[offset]
|
||||
b1 = all_data[offset + 1]
|
||||
opcode = b0 & 0x0F
|
||||
length = b1 & 0x7F
|
||||
offset += 2
|
||||
if length == 126:
|
||||
if offset + 2 > len(all_data): break
|
||||
length = struct.unpack('>H', all_data[offset:offset+2])[0]
|
||||
offset += 2
|
||||
elif length == 127:
|
||||
if offset + 8 > len(all_data): break
|
||||
length = struct.unpack('>Q', all_data[offset:offset+8])[0]
|
||||
offset += 8
|
||||
|
||||
if offset + length > len(all_data):
|
||||
print(f" [W] 帧不完整: 需要 {length} 字节但只剩 {len(all_data)-offset}")
|
||||
break
|
||||
payload = all_data[offset:offset+length]
|
||||
offset += length
|
||||
|
||||
if opcode == 0x01: # text
|
||||
print(f" [text帧] {payload.decode('utf-8', errors='replace')[:500]}")
|
||||
elif opcode == 0x02: # binary
|
||||
try:
|
||||
obj = json.loads(payload.decode('utf-8'))
|
||||
print(f" [binary帧] JSON: type={obj.get('type','?')}, keys={list(obj.keys())}")
|
||||
if obj.get('type') == 'cmd_list':
|
||||
print(f" cmd_list: {len(obj.get('cmds',[]))} 个命令")
|
||||
except:
|
||||
print(f" [binary帧] {len(payload)} 字节: {payload[:200]}")
|
||||
elif opcode == 0x08:
|
||||
print(f" [close帧]")
|
||||
elif opcode == 0x09:
|
||||
print(f" [ping帧]")
|
||||
elif opcode == 0x0A:
|
||||
print(f" [pong帧]")
|
||||
else:
|
||||
print(f" [opcode={opcode}帧] {len(payload)} 字节")
|
||||
|
||||
sock.close()
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
"# Codex 项目权限配置 — 预批准 WSL 环境下操作 RTU 项目的命令"
|
||||
"# 这是安全的关键文件,请谨慎添加规则"
|
||||
""
|
||||
"[commands]"
|
||||
"# RTU 项目根目录操作"
|
||||
"allow = ["
|
||||
" [""find"", ""/home/ypc/RTU/RTU/""],"
|
||||
" [""cat"", ""/home/ypc/RTU/RTU/""],"
|
||||
" [""head"", ""/home/ypc/RTU/RTU/""],"
|
||||
" [""tail"", ""/home/ypc/RTU/RTU/""],"
|
||||
" [""cd"", ""/home/ypc/RTU/RTU""],"
|
||||
" [""git"", ""-C"", ""/home/ypc/RTU/RTU""],"
|
||||
" [""make"", ""-C"", ""/home/ypc/RTU/RTU/release""],"
|
||||
" [""./release/build.sh""],"
|
||||
" [""./test/RTU""],"
|
||||
" [""ls"", ""-la"", ""/home/ypc/RTU/""],"
|
||||
"]"
|
||||
""
|
||||
|
|
@ -631,7 +631,7 @@ int PLC_Logic(void) {
|
|||
|
||||
extract_hw_points_from_graphs(graphs, graph_count, &hw, 0);
|
||||
|
||||
#ifdef DEBUG
|
||||
#ifndef DEBUG
|
||||
printf("==================== 文件解析结果 ====================\n");
|
||||
printf("共解析到 %d 个逻辑图\n", graph_count);
|
||||
for (int i = 0; i < graph_count; i++) {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -927,10 +927,40 @@ void ws_recv(struct mg_connection *c, const char* p_rx, uint16_t rx_len)
|
|||
if(0 == curd_str.compare("add"))
|
||||
{
|
||||
method.add_signal(s, saddr_str);
|
||||
|
||||
/* 添加后立即推送当前信号列表,前端无需等 ws_task 定时轮询 */
|
||||
cJSON *push = cJSON_CreateObject();
|
||||
if(push)
|
||||
{
|
||||
bool dummy = false;
|
||||
method.make_signal_json(s, push, dummy);
|
||||
char *p_tx = cJSON_Print(push);
|
||||
if(p_tx)
|
||||
{
|
||||
ws_send_one(c->id, p_tx, strlen(p_tx));
|
||||
free(p_tx);
|
||||
}
|
||||
cJSON_Delete(push);
|
||||
}
|
||||
}
|
||||
else if(0 == curd_str.compare("del"))
|
||||
{
|
||||
method.del_signal(s, saddr_str);
|
||||
|
||||
/* 删除后立即推送当前信号列表,前端无需等 ws_task 定时轮询 */
|
||||
cJSON *push = cJSON_CreateObject();
|
||||
if(push)
|
||||
{
|
||||
bool dummy = false;
|
||||
method.make_signal_json(s, push, dummy);
|
||||
char *p_tx = cJSON_Print(push);
|
||||
if(p_tx)
|
||||
{
|
||||
ws_send_one(c->id, p_tx, strlen(p_tx));
|
||||
free(p_tx);
|
||||
}
|
||||
cJSON_Delete(push);
|
||||
}
|
||||
}
|
||||
else if(0 == curd_str.compare("set"))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -738,3 +738,109 @@ body.login-page #main-content { margin-left: 0; display: flex; align-items: cent
|
|||
cursor: pointer;
|
||||
accent-color: var(--primary);
|
||||
}
|
||||
|
||||
/* === PLC Debug Page === */
|
||||
.plc-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 16px;
|
||||
background: var(--card-bg);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
.plc-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 16px;
|
||||
background: var(--card-bg);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
.plc-grid {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
}
|
||||
.plc-col {
|
||||
flex: 1;
|
||||
background: var(--card-bg);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
padding: 16px;
|
||||
min-height: 200px;
|
||||
}
|
||||
.plc-col h3 {
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 2px solid var(--primary);
|
||||
font-size: 16px;
|
||||
}
|
||||
.plc-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 8px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
.plc-led {
|
||||
font-size: 28px;
|
||||
cursor: pointer;
|
||||
opacity: 0.3;
|
||||
transition: all 0.3s;
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
.plc-led-on {
|
||||
opacity: 1;
|
||||
filter: none;
|
||||
text-shadow: 0 0 10px #4CAF50, 0 0 20px #4CAF50;
|
||||
}
|
||||
.plc-btn-icon {
|
||||
font-size: 28px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.15s;
|
||||
}
|
||||
.plc-btn-icon:active {
|
||||
transform: scale(0.85);
|
||||
}
|
||||
.plc-info {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
}
|
||||
.plc-saddr {
|
||||
color: var(--primary);
|
||||
font-weight: 600;
|
||||
font-family: monospace;
|
||||
}
|
||||
.plc-val {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--info);
|
||||
min-width: 40px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* === Modal === */
|
||||
.modal-overlay {
|
||||
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0,0,0,0.45);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
z-index: 999;
|
||||
}
|
||||
.modal-box {
|
||||
background: var(--card-bg);
|
||||
border-radius: var(--radius);
|
||||
padding: 24px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.2);
|
||||
min-width: 380px;
|
||||
}
|
||||
.modal-box h3 { margin-bottom: 8px; }
|
||||
.plc-sig-row:hover { background: #f0f8ff !important; }
|
||||
|
||||
/* === Batch ops === */
|
||||
.batch-ops { display: flex; align-items: center; gap: 8px; }
|
||||
|
||||
/* === Dashboard plc card === */
|
||||
.dash-card.plc { border-top: 4px solid #FF9800; }
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@
|
|||
<script src="js/monitor.js"></script>
|
||||
<script src="js/ws-client.js"></script>
|
||||
<script src="js/pages.js"></script>
|
||||
<script src="js/plc_debug.js"></script>
|
||||
<script src="js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ var App = (function() {
|
|||
{ hash: '#ao', label: '🔧 参数 (ao)' },
|
||||
{ hash: '#param', label: '⚙ 定值 (param)' },
|
||||
{ hash: '#datacenter', label: '📊 数据中心' },
|
||||
{ hash: '#plc_debug', label: '🔌 PLC调试' },
|
||||
{ hash: '#monitor', label: '📋 数据监控' },
|
||||
{ hash: '#terminal', label: '💻 命令终端' }
|
||||
];
|
||||
|
|
@ -112,7 +113,7 @@ var App = (function() {
|
|||
}
|
||||
|
||||
// 页面进入后立即推送缓存数据(如有)
|
||||
var stMap = { '#out': 'out', '#in': 'in', '#yk': 'yk', '#ao': 'ao', '#param': 'param', '#datacenter': 'dc_data', '#terminal': 'cmd_list' };
|
||||
var stMap = { '#out': 'out', '#in': 'in', '#yk': 'yk', '#ao': 'ao', '#param': 'param', '#datacenter': 'dc_data', '#plc_debug': 'out', '#terminal': 'cmd_list' };
|
||||
var st = stMap[hash];
|
||||
if (st && page.onData && lastDataCache[st]) {
|
||||
page.onData(lastDataCache[st]);
|
||||
|
|
@ -201,7 +202,8 @@ var App = (function() {
|
|||
'#in': 'in',
|
||||
'#yk': 'yk',
|
||||
'#ao': 'ao',
|
||||
'#param': 'param'
|
||||
'#param': 'param',
|
||||
'#plc_debug': 'out'
|
||||
};
|
||||
|
||||
var signalType = signalTypeMap[currentHash];
|
||||
|
|
@ -221,6 +223,7 @@ var App = (function() {
|
|||
registerPage('#ao', AoPage);
|
||||
registerPage('#param', ParamPage);
|
||||
registerPage('#datacenter', DatacenterPage);
|
||||
registerPage('#plc_debug', PlcDebugPage);
|
||||
registerPage('#monitor', MonitorPage);
|
||||
registerPage('#terminal', TerminalPage);
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,20 @@
|
|||
|
||||
var CTRL_TYPE_MAP = { 0: '只读', 1: '直控', 2: '选控' };
|
||||
|
||||
/* 每种信号类型的最大注册数量 */
|
||||
var MAX_SIGNALS_PER_TYPE = 500;
|
||||
|
||||
/**
|
||||
* 获取指定信号类型已注册的数量
|
||||
* @param {string} signalType - out/in/yk/ao/param
|
||||
* @returns {number}
|
||||
*/
|
||||
function getRegisteredCount(signalType) {
|
||||
var data = (typeof App !== 'undefined' && App.getSignalData)
|
||||
? App.getSignalData(signalType) : null;
|
||||
return (data && Array.isArray(data)) ? data.length : 0;
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
* 共用工具
|
||||
* ======================================================================== */
|
||||
|
|
@ -201,9 +215,9 @@ function importAllFromXml() {
|
|||
input.click();
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
/* ------------------------------------------------------------------------
|
||||
* 共用工具栏 HTML
|
||||
* ======================================================================== */
|
||||
* ------------------------------------------------------------------------ */
|
||||
function makeToolbar(signalType, showSet) {
|
||||
var typeLabel = signalType === 'ao' ? '参数' : (signalType === 'param' ? '定值' : signalType);
|
||||
return ''
|
||||
|
|
@ -239,6 +253,13 @@ function bindToolbar(signalType, getDataFn) {
|
|||
alert('请输入短地址 saddr');
|
||||
return;
|
||||
}
|
||||
if (curd === 'add') {
|
||||
var cur = getRegisteredCount(signalType);
|
||||
if (cur >= MAX_SIGNALS_PER_TYPE) {
|
||||
alert(signalType + ' 已注册 ' + cur + ' 个,最多允许 ' + MAX_SIGNALS_PER_TYPE + ' 个,请先删除旧信号');
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!WsClient.send({ saddr: saddr, signal_type: signalType, curd: curd, setting_zone: '0', signal_data: '' })) {
|
||||
alert('WebSocket 未连接');
|
||||
}
|
||||
|
|
@ -291,7 +312,8 @@ var DashboardPage = {
|
|||
{ cls: 'ao', icon: '🔧', title: '参数', desc: 'ao — 模拟输出参数设定', hash: '#ao' },
|
||||
{ cls: 'param', icon: '⚙', title: '定值', desc: 'param — 多定值区参数管理', hash: '#param' },
|
||||
{ cls: 'monitor', icon: '📋', title: '数据监控', desc: '实时查看 WebSocket 交互数据', hash: '#monitor' },
|
||||
{ cls: 'terminal', icon: '💻', title: '命令终端', desc: '实时命令行操作 RTU 系统', hash: '#terminal' }
|
||||
{ cls: 'terminal', icon: '💻', title: '命令终端', desc: '实时命令行操作 RTU 系统', hash: '#terminal' },
|
||||
{ cls: 'plc', icon: '🔌', title: 'PLC调试', desc: '指示灯/按键绑定 out 信号调试', hash: '#plc_debug' }
|
||||
];
|
||||
var html = '<div class="page-header"><h2>功能导航</h2><p>选择需要管理的信号类型进入对应功能页</p></div>';
|
||||
html += '<div class="dashboard-grid">';
|
||||
|
|
@ -324,17 +346,38 @@ var DashboardPage = {
|
|||
* ======================================================================== */
|
||||
var OutPage = (function() {
|
||||
var data = [];
|
||||
var selected = {}; /* {idx: true} 行选中状态 */
|
||||
|
||||
function getCheckedItems() {
|
||||
var items = [];
|
||||
for (var k in selected) {
|
||||
if (selected.hasOwnProperty(k) && data[parseInt(k)]) {
|
||||
items.push(data[parseInt(k)]);
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
function updateDelBtn() {
|
||||
var btn = document.getElementById('out-del-btn');
|
||||
if (!btn) return;
|
||||
var checked = document.querySelectorAll('#out-tbody .row-cb:checked');
|
||||
var cnt = checked.length;
|
||||
btn.textContent = '🗑 删除选中' + (cnt > 0 ? '(' + cnt + ')' : '');
|
||||
btn.disabled = (cnt === 0);
|
||||
}
|
||||
function renderTableHtml() {
|
||||
if (data.length === 0) return '';
|
||||
var rows = '';
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
var item = data[i];
|
||||
var checked = !!selected[i];
|
||||
rows += '<tr>'
|
||||
+ '<td><input type="checkbox" class="row-cb" data-idx="' + i + '"' + (checked ? ' checked' : '') + '></td>'
|
||||
+ '<td>' + (i + 1) + '</td>'
|
||||
+ '<td>' + fmtVal(item.saddr) + '</td>'
|
||||
+ '<td>' + fmtVal(item.desc) + '</td>'
|
||||
+ '<td>' + fmtVal(item.type) + '</td>'
|
||||
+ '<td class="val-cell">' + fmtVal(item.val) + '</td>'
|
||||
+ '<td class="val-cell"><span id="out-val-' + i + '">' + fmtVal(item.val) + '</span></td>'
|
||||
+ '<td><input class="val-input" id="mod-' + i + '" placeholder="修改值"></td>'
|
||||
+ '<td><button class="btn btn-sm btn-info" data-idx="' + i + '">修改</button></td>'
|
||||
+ '</tr>';
|
||||
|
|
@ -353,26 +396,60 @@ var OutPage = (function() {
|
|||
WsClient.send({ curd:'set', signal_type:'out', saddr:item.saddr, signal_data:val, setting_zone:'0' });
|
||||
};
|
||||
}
|
||||
/* 绑定行复选框 */
|
||||
var cbs = document.querySelectorAll('#out-tbody .row-cb');
|
||||
for (var j = 0; j < cbs.length; j++) {
|
||||
cbs[j].onclick = function(e) {
|
||||
e.stopPropagation();
|
||||
var idx = this.getAttribute('data-idx');
|
||||
if (this.checked) selected[idx] = true; else delete selected[idx];
|
||||
updateDelBtn();
|
||||
};
|
||||
}
|
||||
}
|
||||
function doDeleteChecked() {
|
||||
var items = getCheckedItems();
|
||||
if (items.length === 0) { alert('请先勾选要删除的信号'); return; }
|
||||
if (!confirm('确定删除 ' + items.length + ' 个信号?')) return;
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
WsClient.send({ curd:'del', signal_type:'out', saddr:items[i].saddr, setting_zone:'0', signal_data:'' });
|
||||
}
|
||||
selected = {};
|
||||
updateDelBtn();
|
||||
}
|
||||
return {
|
||||
render: function(container) {
|
||||
var headers = ['#', 'saddr', 'desc', '数据类型', 'val', '修改值', '修改'];
|
||||
selected = {};
|
||||
var headers = ['', '#', 'saddr', 'desc', '数据类型', 'val', '修改值', '修改'];
|
||||
container.innerHTML = ''
|
||||
+ '<div class="page-header"><h2>注册管理</h2><p>out — 状态量信号,注册后支持添加、删除、修改值</p></div>'
|
||||
+ makeToolbar('out')
|
||||
+ '<div class="batch-ops" style="margin-bottom:12px"><button class="btn btn-danger" id="out-del-btn" disabled>🗑 删除选中</button></div>'
|
||||
+ renderTable('out-tbody', headers, renderTableHtml(), '暂无注册数据,请先添加信号');
|
||||
bindToolbar('out', function() { return data; });
|
||||
bindModifyButtons();
|
||||
var delBtn = document.getElementById('out-del-btn');
|
||||
if (delBtn) delBtn.onclick = doDeleteChecked;
|
||||
updateDelBtn();
|
||||
},
|
||||
onEnter: function() {},
|
||||
onLeave: function() { data = []; },
|
||||
onLeave: function() { data = []; selected = {}; },
|
||||
onData: function(arr) {
|
||||
data = arr || [];
|
||||
var tbody = document.getElementById('out-tbody');
|
||||
if (!tbody) return;
|
||||
if (data.length === 0) { tbody.innerHTML = '<tr class="empty-row"><td colspan="7">暂无注册数据,请先添加信号</td></tr>'; return; }
|
||||
tbody.innerHTML = renderTableHtml();
|
||||
bindModifyButtons();
|
||||
if (data.length === 0) { tbody.innerHTML = '<tr class="empty-row"><td colspan="8">暂无注册数据,请先添加信号</td></tr>'; updateDelBtn(); return; }
|
||||
var oldRowCnt = tbody.querySelectorAll('tr:not(.empty-row)').length;
|
||||
if (oldRowCnt !== data.length) {
|
||||
tbody.innerHTML = renderTableHtml();
|
||||
bindModifyButtons();
|
||||
} else {
|
||||
var valCells = tbody.querySelectorAll('.val-cell');
|
||||
for (var v = 0; v < data.length && v < valCells.length; v++) {
|
||||
valCells[v].textContent = fmtVal(data[v].val);
|
||||
}
|
||||
}
|
||||
updateDelBtn();
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
|
@ -382,43 +459,86 @@ var OutPage = (function() {
|
|||
* ======================================================================== */
|
||||
var InPage = (function() {
|
||||
var data = [];
|
||||
var selected = {};
|
||||
|
||||
function getCheckedItems() {
|
||||
var items = [];
|
||||
for (var k in selected) {
|
||||
if (selected.hasOwnProperty(k) && data[parseInt(k)]) items.push(data[parseInt(k)]);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
function updateDelBtn() {
|
||||
var btn = document.getElementById('in-del-btn');
|
||||
if (!btn) return;
|
||||
var cnt = 0;
|
||||
for (var k in selected) { if (selected.hasOwnProperty(k)) cnt++; }
|
||||
btn.textContent = '🗑 删除选中' + (cnt > 0 ? '(' + cnt + ')' : '');
|
||||
btn.disabled = (cnt === 0);
|
||||
}
|
||||
function renderTableHtml() {
|
||||
if (data.length === 0) return '';
|
||||
var rows = '';
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
var item = data[i];
|
||||
var checked = !!selected[i];
|
||||
rows += '<tr>'
|
||||
+ '<td><input type="checkbox" class="row-cb" data-idx="' + i + '"' + (checked ? ' checked' : '') + '></td>'
|
||||
+ '<td>' + (i + 1) + '</td>'
|
||||
+ '<td>' + fmtVal(item.saddr) + '</td>'
|
||||
+ '<td>' + fmtVal(item.desc) + '</td>'
|
||||
+ '<td>' + fmtVal(item.type) + '</td>'
|
||||
+ '<td class="val-cell">' + fmtVal(item.val) + '</td>'
|
||||
+ '<td class="val-cell"><span id="in-val-' + i + '">' + fmtVal(item.val) + '</span></td>'
|
||||
+ '</tr>';
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
function bindCb() {
|
||||
var cbs = document.querySelectorAll('#in-tbody .row-cb');
|
||||
for (var j = 0; j < cbs.length; j++) {
|
||||
cbs[j].onclick = function(e) {
|
||||
e.stopPropagation();
|
||||
var idx = this.getAttribute('data-idx');
|
||||
if (this.checked) selected[idx] = true; else delete selected[idx];
|
||||
updateDelBtn();
|
||||
};
|
||||
}
|
||||
}
|
||||
function doDeleteChecked() {
|
||||
var items = getCheckedItems();
|
||||
if (items.length === 0) { alert('请先勾选要删除的信号'); return; }
|
||||
if (!confirm('确定删除 ' + items.length + ' 个信号?')) return;
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
WsClient.send({ curd:'del', signal_type:'in', saddr:items[i].saddr, setting_zone:'0', signal_data:'' });
|
||||
}
|
||||
selected = {};
|
||||
updateDelBtn();
|
||||
}
|
||||
return {
|
||||
render: function(container) {
|
||||
var headers = ['#', 'saddr', 'desc', '数据类型', 'val'];
|
||||
selected = {};
|
||||
var headers = ['', '#', 'saddr', 'desc', '数据类型', 'val'];
|
||||
container.innerHTML = ''
|
||||
+ '<div class="page-header"><h2>链接注册管理</h2><p>in — 测量量信号,链接注册后仅支持添加和删除,不支持修改</p></div>'
|
||||
+ makeToolbar('in')
|
||||
+ '<div class="batch-ops" style="margin-bottom:12px"><button class="btn btn-danger" id="in-del-btn" disabled>🗑 删除选中</button></div>'
|
||||
+ renderTable('in-tbody', headers, renderTableHtml(), '暂无链接注册数据,请先添加信号');
|
||||
bindToolbar('in', function() { return data; });
|
||||
bindCb();
|
||||
var delBtn = document.getElementById('in-del-btn');
|
||||
if (delBtn) delBtn.onclick = doDeleteChecked;
|
||||
updateDelBtn();
|
||||
},
|
||||
onEnter: function() {},
|
||||
onLeave: function() { data = []; },
|
||||
onLeave: function() { data = []; selected = {}; },
|
||||
onData: function(arr) {
|
||||
data = arr || [];
|
||||
var tbody = document.getElementById('in-tbody');
|
||||
if (!tbody) return;
|
||||
if (data.length === 0) {
|
||||
tbody.innerHTML = '<tr class="empty-row"><td colspan="5">暂无链接注册数据,请先添加信号</td></tr>';
|
||||
return;
|
||||
}
|
||||
if (data.length === 0) { tbody.innerHTML = '<tr class="empty-row"><td colspan="6">暂无链接注册数据,请先添加信号</td></tr>'; updateDelBtn(); return; }
|
||||
tbody.innerHTML = renderTableHtml();
|
||||
bindCb();
|
||||
updateDelBtn();
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
|
@ -428,27 +548,46 @@ var InPage = (function() {
|
|||
* ======================================================================== */
|
||||
var YkPage = (function() {
|
||||
var data = [];
|
||||
var selected = {};
|
||||
|
||||
function getCheckedItems() {
|
||||
var items = [];
|
||||
for (var k in selected) { if (selected.hasOwnProperty(k) && data[parseInt(k)]) items.push(data[parseInt(k)]); }
|
||||
return items;
|
||||
}
|
||||
function updateDelBtn() {
|
||||
var btn = document.getElementById('yk-del-btn');
|
||||
if (!btn) return;
|
||||
var cnt = 0;
|
||||
for (var k in selected) { if (selected.hasOwnProperty(k)) cnt++; }
|
||||
btn.textContent = '🗑 删除选中' + (cnt > 0 ? '(' + cnt + ')' : '');
|
||||
btn.disabled = (cnt === 0);
|
||||
}
|
||||
function renderTableHtml() {
|
||||
if (data.length === 0) return '';
|
||||
var rows = '';
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
var item = data[i];
|
||||
var checked = !!selected[i];
|
||||
var ctrlLabel = CTRL_TYPE_MAP[item.ctrl_type] || item.ctrl_type;
|
||||
rows += '<tr>'
|
||||
+ '<td><input type="checkbox" class="row-cb" data-idx="' + i + '"' + (checked ? ' checked' : '') + '></td>'
|
||||
+ '<td>' + (i + 1) + '</td>'
|
||||
+ '<td>' + fmtVal(item.saddr) + '</td>'
|
||||
+ '<td>' + fmtVal(item.desc) + '</td>'
|
||||
+ '<td>' + fmtVal(item.type) + '</td>'
|
||||
+ '<td class="val-cell">' + fmtVal(item.val) + '</td>'
|
||||
+ '<td>' + ctrlLabel + '</td>'
|
||||
+ '<td><button class="btn btn-sm btn-success" data-idx="' + i + '" data-val="1">控合</button></td>'
|
||||
+ '<td><button class="btn btn-sm btn-danger" data-idx="' + i + '" data-val="0">控分</button></td>'
|
||||
+ '<td>' + fmtVal(item.min) + '</td>'
|
||||
+ '<td>' + fmtVal(item.max) + '</td>'
|
||||
+ '<td>' + fmtVal(item.step) + '</td>'
|
||||
+ '<td>' + fmtVal(item.unit) + '</td>'
|
||||
+ '<td><input class="val-input" id="yk-mod-' + i + '" placeholder="修改值"></td>'
|
||||
+ '<td><button class="btn btn-sm btn-info" data-idx="' + i + '">修改</button></td>'
|
||||
+ '</tr>';
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
function bindButtons() {
|
||||
var buttons = document.querySelectorAll('#yk-tbody .btn-sm');
|
||||
for (var k = 0; k < buttons.length; k++) {
|
||||
|
|
@ -456,36 +595,50 @@ var YkPage = (function() {
|
|||
var idx = parseInt(this.getAttribute('data-idx'));
|
||||
var val = this.getAttribute('data-val');
|
||||
var item = data[idx];
|
||||
WsClient.send({
|
||||
curd: 'set', signal_type: 'yk', saddr: item.saddr,
|
||||
signal_data: val, setting_zone: '0'
|
||||
});
|
||||
WsClient.send({ curd: 'set', signal_type: 'yk', saddr: item.saddr, signal_data: val, setting_zone: '0' });
|
||||
};
|
||||
}
|
||||
var cbs = document.querySelectorAll('#yk-tbody .row-cb');
|
||||
for (var j = 0; j < cbs.length; j++) {
|
||||
cbs[j].onclick = function(e) { e.stopPropagation(); var idx = this.getAttribute('data-idx'); if (this.checked) selected[idx] = true; else delete selected[idx]; updateDelBtn(); };
|
||||
}
|
||||
}
|
||||
function doDeleteChecked() {
|
||||
var items = getCheckedItems();
|
||||
if (items.length === 0) { alert('请先勾选要删除的信号'); return; }
|
||||
if (!confirm('确定删除 ' + items.length + ' 个信号?')) return;
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
WsClient.send({ curd:'del', signal_type:'yk', saddr:items[i].saddr, setting_zone:'0', signal_data:'' });
|
||||
}
|
||||
selected = {};
|
||||
updateDelBtn();
|
||||
}
|
||||
|
||||
return {
|
||||
render: function(container) {
|
||||
var headers = ['#', 'saddr', 'desc', '数据类型', 'val', 'ctrl_type', '控合', '控分'];
|
||||
selected = {};
|
||||
var headers = ['', '#', 'saddr', 'desc', '数据类型', 'val', 'ctrl_type', 'min', 'max', 'step', 'unit', '修改值', '修改'];
|
||||
container.innerHTML = ''
|
||||
+ '<div class="page-header"><h2>遥控管理</h2><p>yk — 遥控分合闸控制,控合=1(闭合),控分=0(断开)</p></div>'
|
||||
+ makeToolbar('yk')
|
||||
+ '<div class="batch-ops" style="margin-bottom:12px"><button class="btn btn-danger" id="yk-del-btn" disabled>🗑 删除选中</button></div>'
|
||||
+ renderTable('yk-tbody', headers, renderTableHtml(), '暂无遥控数据,请先添加信号');
|
||||
bindToolbar('yk', function() { return data; });
|
||||
bindButtons();
|
||||
var delBtn = document.getElementById('yk-del-btn');
|
||||
if (delBtn) delBtn.onclick = doDeleteChecked;
|
||||
updateDelBtn();
|
||||
},
|
||||
onEnter: function() {},
|
||||
onLeave: function() { data = []; },
|
||||
onLeave: function() { data = []; selected = {}; },
|
||||
onData: function(arr) {
|
||||
data = arr || [];
|
||||
// 不重置 selected,保留用户勾选
|
||||
var tbody = document.getElementById('yk-tbody');
|
||||
if (!tbody) return;
|
||||
if (data.length === 0) {
|
||||
tbody.innerHTML = '<tr class="empty-row"><td colspan="8">暂无遥控数据,请先添加信号</td></tr>';
|
||||
return;
|
||||
}
|
||||
if (data.length === 0) { tbody.innerHTML = '<tr class="empty-row"><td colspan="9">暂无遥控数据,请先添加信号</td></tr>'; updateDelBtn(); return; }
|
||||
tbody.innerHTML = renderTableHtml();
|
||||
bindButtons();
|
||||
updateDelBtn();
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
|
@ -495,14 +648,18 @@ var YkPage = (function() {
|
|||
* ======================================================================== */
|
||||
var AoPage = (function() {
|
||||
var data = [];
|
||||
var selected = {};
|
||||
|
||||
function getCheckedItems() { var items=[]; for(var k in selected){if(selected.hasOwnProperty(k)&&data[parseInt(k)])items.push(data[parseInt(k)]);} return items; }
|
||||
function updateDelBtn() { var btn=document.getElementById('ao-del-btn'); if(!btn)return; var cnt=0; for(var k in selected){if(selected.hasOwnProperty(k))cnt++;} btn.textContent='🗑 删除选中'+(cnt>0?'('+cnt+')':''); btn.disabled=(cnt===0); }
|
||||
function renderTableHtml() {
|
||||
if (data.length === 0) return '';
|
||||
var rows = '';
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
var item = data[i];
|
||||
var item = data[i]; var checked = !!selected[i];
|
||||
var ctrlLabel = CTRL_TYPE_MAP[item.ctrl_type] || item.ctrl_type;
|
||||
rows += '<tr>'
|
||||
+ '<td><input type="checkbox" class="row-cb" data-idx="' + i + '"' + (checked ? ' checked' : '') + '></td>'
|
||||
+ '<td>' + (i + 1) + '</td>'
|
||||
+ '<td>' + fmtVal(item.saddr) + '</td>'
|
||||
+ '<td>' + fmtVal(item.desc) + '</td>'
|
||||
|
|
@ -519,46 +676,39 @@ var AoPage = (function() {
|
|||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
function bindModifyButtons() {
|
||||
function bindAll() {
|
||||
var buttons = document.querySelectorAll('#ao-tbody .btn-info');
|
||||
for (var k = 0; k < buttons.length; k++) {
|
||||
buttons[k].onclick = function() {
|
||||
var idx = parseInt(this.getAttribute('data-idx'));
|
||||
var input = document.getElementById('ao-mod-' + idx);
|
||||
var val = input.value.trim();
|
||||
if (val === '') { alert('请输入修改值'); return; }
|
||||
var item = data[idx];
|
||||
WsClient.send({
|
||||
curd: 'set', signal_type: 'ao', saddr: item.saddr,
|
||||
signal_data: val, setting_zone: '0'
|
||||
});
|
||||
};
|
||||
}
|
||||
for (var k = 0; k < buttons.length; k++) { buttons[k].onclick=function(){var idx=parseInt(this.getAttribute('data-idx'));var input=document.getElementById('ao-mod-'+idx);var val=input.value.trim();if(val===''){alert('请输入修改值');return;}var item=data[idx]; WsClient.send({curd:'set',signal_type:'ao',saddr:item.saddr,signal_data:val,setting_zone:'0'});};}
|
||||
var cbs = document.querySelectorAll('#ao-tbody .row-cb');
|
||||
for (var j = 0; j < cbs.length; j++) { cbs[j].onclick=function(e){e.stopPropagation();var idx=this.getAttribute('data-idx');if(this.checked)selected[idx]=true;else delete selected[idx];updateDelBtn();};}
|
||||
}
|
||||
|
||||
function doDeleteChecked() { var items=getCheckedItems(); if(items.length===0){alert('请先勾选要删除的信号');return;} if(!confirm('确定删除 '+items.length+' 个信号?'))return; for(var i=0;i<items.length;i++){WsClient.send({curd:'del',signal_type:'ao',saddr:items[i].saddr,setting_zone:'0',signal_data:''});} selected={}; updateDelBtn(); }
|
||||
return {
|
||||
render: function(container) {
|
||||
var headers = ['#', 'saddr', 'desc', '数据类型', 'val', 'ctrl_type', 'min', 'max', 'step', 'unit', '修改值', '修改'];
|
||||
selected = {};
|
||||
var headers = ['', '#', 'saddr', 'desc', '数据类型', 'val', 'ctrl_type', 'min', 'max', 'step', 'unit', '修改值', '修改'];
|
||||
container.innerHTML = ''
|
||||
+ '<div class="page-header"><h2>参数管理</h2><p>ao — 模拟输出参数设定,支持单值修改</p></div>'
|
||||
+ makeToolbar('ao')
|
||||
+ '<div class="batch-ops" style="margin-bottom:12px"><button class="btn btn-danger" id="ao-del-btn" disabled>🗑 删除选中</button></div>'
|
||||
+ renderTable('ao-tbody', headers, renderTableHtml(), '暂无参数数据,请先添加信号');
|
||||
bindToolbar('ao', function() { return data; });
|
||||
bindModifyButtons();
|
||||
bindAll();
|
||||
var delBtn = document.getElementById('ao-del-btn');
|
||||
if (delBtn) delBtn.onclick = doDeleteChecked;
|
||||
updateDelBtn();
|
||||
},
|
||||
onEnter: function() {},
|
||||
onLeave: function() { data = []; },
|
||||
onLeave: function() { data = []; selected = {}; },
|
||||
onData: function(arr) {
|
||||
data = arr || [];
|
||||
// 不重置 selected,保留用户勾选
|
||||
var tbody = document.getElementById('ao-tbody');
|
||||
if (!tbody) return;
|
||||
if (data.length === 0) {
|
||||
tbody.innerHTML = '<tr class="empty-row"><td colspan="12">暂无参数数据,请先添加信号</td></tr>';
|
||||
return;
|
||||
}
|
||||
if (data.length === 0) { tbody.innerHTML = '<tr class="empty-row"><td colspan="13">暂无参数数据,请先添加信号</td></tr>'; updateDelBtn(); return; }
|
||||
tbody.innerHTML = renderTableHtml();
|
||||
bindModifyButtons();
|
||||
bindAll();
|
||||
updateDelBtn();
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
|
@ -568,7 +718,11 @@ var AoPage = (function() {
|
|||
* ======================================================================== */
|
||||
var ParamPage = (function() {
|
||||
var data = [];
|
||||
var expandedRows = {}; // 记录展开状态
|
||||
var expandedRows = {};
|
||||
var selected = {};
|
||||
|
||||
function getCheckedItems() { var items=[]; for(var k in selected){if(selected.hasOwnProperty(k)&&data[parseInt(k)])items.push(data[parseInt(k)]);} return items; }
|
||||
function updateDelBtn() { var btn=document.getElementById('param-del-btn'); if(!btn)return; var cnt=0; for(var k in selected){if(selected.hasOwnProperty(k))cnt++;} btn.textContent='🗑 删除选中'+(cnt>0?'('+cnt+')':''); btn.disabled=(cnt===0); }
|
||||
|
||||
function renderTableHtml() {
|
||||
if (data.length === 0) return '';
|
||||
|
|
@ -581,8 +735,10 @@ var ParamPage = (function() {
|
|||
var isOpen = expandedRows[i] === true;
|
||||
var icon = isOpen ? '▼' : '▶';
|
||||
|
||||
var checked = !!selected[i];
|
||||
// 主行(可点击展开)
|
||||
rows += '<tr class="expand-row" data-pidx="' + i + '">'
|
||||
+ '<td><input type="checkbox" class="row-cb" data-idx="' + i + '"' + (checked ? ' checked' : '') + '></td>'
|
||||
+ '<td>' + (i + 1) + '</td>'
|
||||
+ '<td>' + fmtVal(item.saddr) + '</td>'
|
||||
+ '<td>' + fmtVal(item.desc) + '</td>'
|
||||
|
|
@ -594,7 +750,7 @@ var ParamPage = (function() {
|
|||
|
||||
// 展开的定值区子表
|
||||
if (isOpen) {
|
||||
rows += '<tr class="zone-sub-row" id="zone-sub-' + i + '"><td colspan="7">'
|
||||
rows += '<tr class="zone-sub-row" id="zone-sub-' + i + '"><td colspan="8">'
|
||||
+ '<table class="zone-sub-table">'
|
||||
+ '<thead><tr>'
|
||||
+ '<th>定值区 ID</th><th>val</th><th>default_val</th>'
|
||||
|
|
@ -624,67 +780,59 @@ var ParamPage = (function() {
|
|||
}
|
||||
|
||||
function bindEvents() {
|
||||
// 展开/收起
|
||||
var expandRows = document.querySelectorAll('#param-tbody .expand-row');
|
||||
for (var k = 0; k < expandRows.length; k++) {
|
||||
expandRows[k].onclick = function() {
|
||||
expandRows[k].onclick = function(e) {
|
||||
if (e.target.tagName === 'INPUT') return; // 不拦截勾选框
|
||||
var idx = parseInt(this.getAttribute('data-pidx'));
|
||||
expandedRows[idx] = !expandedRows[idx];
|
||||
refreshTable();
|
||||
};
|
||||
}
|
||||
|
||||
// 修改按钮
|
||||
var modButtons = document.querySelectorAll('#param-tbody .btn-info');
|
||||
for (var m = 0; m < modButtons.length; m++) {
|
||||
modButtons[m].onclick = function(e) {
|
||||
e.stopPropagation();
|
||||
var pidx = parseInt(this.getAttribute('data-pidx'));
|
||||
var zidx = parseInt(this.getAttribute('data-zidx'));
|
||||
var input = document.getElementById('pm-' + pidx + '-' + zidx);
|
||||
var val = input.value.trim();
|
||||
if (val === '') { alert('请输入修改值'); return; }
|
||||
var item = data[pidx];
|
||||
var zoneId = item.setting_zone_list[zidx].id;
|
||||
WsClient.send({
|
||||
curd: 'set', signal_type: 'param', saddr: item.saddr,
|
||||
signal_data: val, setting_zone: zoneId
|
||||
});
|
||||
};
|
||||
modButtons[m].onclick = function(e) { e.stopPropagation(); var pidx=parseInt(this.getAttribute('data-pidx')); var zidx=parseInt(this.getAttribute('data-zidx')); var input=document.getElementById('pm-'+pidx+'-'+zidx); var val=input.value.trim(); if(val===''){alert('请输入修改值');return;} var item=data[pidx]; var zoneId=item.setting_zone_list[zidx].id; WsClient.send({curd:'set',signal_type:'param',saddr:item.saddr,signal_data:val,setting_zone:zoneId});};
|
||||
}
|
||||
var cbs = document.querySelectorAll('#param-tbody .row-cb');
|
||||
for (var j = 0; j < cbs.length; j++) {
|
||||
cbs[j].onclick = function(e) { e.stopPropagation(); var idx = this.getAttribute('data-idx'); if(this.checked)selected[idx]=true; else delete selected[idx]; updateDelBtn(); };
|
||||
}
|
||||
}
|
||||
|
||||
function doDeleteChecked() { var items=getCheckedItems(); if(items.length===0){alert('请先勾选要删除的信号');return;} if(!confirm('确定删除 '+items.length+' 个信号?'))return; for(var i=0;i<items.length;i++){WsClient.send({curd:'del',signal_type:'param',saddr:items[i].saddr,setting_zone:'0',signal_data:''});} selected={}; expandedRows={}; updateDelBtn(); refreshTable(); }
|
||||
function refreshTable() {
|
||||
var tbody = document.getElementById('param-tbody');
|
||||
if (!tbody) return;
|
||||
tbody.innerHTML = renderTableHtml();
|
||||
bindEvents();
|
||||
}
|
||||
|
||||
return {
|
||||
render: function(container) {
|
||||
expandedRows = {};
|
||||
var headers = ['#', 'saddr', 'desc', '数据类型', 'ctrl_type', '定值区数', '展开'];
|
||||
selected = {};
|
||||
var headers = ['', '#', 'saddr', 'desc', '数据类型', 'ctrl_type', '定值区数', '展开'];
|
||||
container.innerHTML = ''
|
||||
+ '<div class="page-header"><h2>定值管理</h2><p>param — 多定值区参数管理,点击行展开查看各定值区详情并可修改</p></div>'
|
||||
+ makeToolbar('param')
|
||||
+ '<div class="batch-ops" style="margin-bottom:12px"><button class="btn btn-danger" id="param-del-btn" disabled>🗑 删除选中</button></div>'
|
||||
+ renderTable('param-tbody', headers, renderTableHtml(), '暂无定值数据,请先添加信号');
|
||||
bindToolbar('param', function() { return data; });
|
||||
bindEvents();
|
||||
var delBtn = document.getElementById('param-del-btn');
|
||||
if (delBtn) delBtn.onclick = doDeleteChecked;
|
||||
updateDelBtn();
|
||||
},
|
||||
onEnter: function() {},
|
||||
onLeave: function() { data = []; expandedRows = {}; },
|
||||
onLeave: function() { data = []; expandedRows = {}; selected = {}; },
|
||||
onData: function(arr) {
|
||||
data = arr || [];
|
||||
expandedRows = {}; // 新数据到达时重置展开状态
|
||||
expandedRows = {};
|
||||
// 不重置 selected,保留用户勾选
|
||||
var tbody = document.getElementById('param-tbody');
|
||||
if (!tbody) return;
|
||||
if (data.length === 0) {
|
||||
tbody.innerHTML = '<tr class="empty-row"><td colspan="7">暂无定值数据,请先添加信号</td></tr>';
|
||||
return;
|
||||
}
|
||||
if (data.length === 0) { tbody.innerHTML = '<tr class="empty-row"><td colspan="8">暂无定值数据,请先添加信号</td></tr>'; updateDelBtn(); return; }
|
||||
tbody.innerHTML = renderTableHtml();
|
||||
bindEvents();
|
||||
updateDelBtn();
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
|
@ -1065,6 +1213,12 @@ var DatacenterPage = (function() {
|
|||
var typeLabels = { out:'注册', in:'链接注册', yk:'遥控', ao:'参数', param:'定值' };
|
||||
var label = typeLabels[activeTab] || activeTab;
|
||||
|
||||
var cur = getRegisteredCount(activeTab);
|
||||
if (cur + items.length > MAX_SIGNALS_PER_TYPE) {
|
||||
alert(label + ' 已有 ' + cur + ' 个,本次选择 ' + items.length + ' 个,合计 ' + (cur + items.length) + ' 个,超过上限 ' + MAX_SIGNALS_PER_TYPE + ' 个。\n请先删除旧信号或减少选择');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm('确定将 ' + items.length + ' 个信号添加到 ' + label + '?')) return;
|
||||
|
||||
var succ = 0;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,275 @@
|
|||
/* ========================================================================
|
||||
* PLC 调试页面 — 指示灯/按键 + out 绑定 + 勾选删除 + 增量刷新
|
||||
* ======================================================================== */
|
||||
var PlcDebugLeds = []; // [{saddr:'', desc:'', val:0}]
|
||||
var PlcDebugButtons = []; // [{saddr:'', desc:'', val:0}]
|
||||
|
||||
var PlcDebugPage = (function() {
|
||||
var outSignals = [];
|
||||
var ledChecked = {}; // {idx: true}
|
||||
var btnChecked = {}; // {idx: true}
|
||||
|
||||
function getOutSignals() {
|
||||
if (typeof App !== 'undefined' && App.getSignalData) {
|
||||
var out = App.getSignalData('out');
|
||||
if (Array.isArray(out) && out.length > 0) return out;
|
||||
}
|
||||
return outSignals;
|
||||
}
|
||||
|
||||
function showSignalPicker(callback) {
|
||||
var sigs = getOutSignals();
|
||||
if (sigs.length === 0) { alert('暂无 out 已注册信号,请先在注册(out)页面添加信号'); return; }
|
||||
var html = '<div class="modal-overlay" id="plc-modal"><div class="modal-box" style="max-width:550px">'
|
||||
+ '<h3>绑定 out 信号</h3>'
|
||||
+ '<div style="max-height:300px;overflow-y:auto;margin:12px 0">';
|
||||
for (var i = 0; i < sigs.length; i++) {
|
||||
var s = sigs[i];
|
||||
html += '<div class="plc-sig-row" data-saddr="' + fmtVal(s.saddr) + '" data-desc="' + fmtVal(s.desc || '') + '" style="padding:8px;cursor:pointer;border-bottom:1px solid #eee">'
|
||||
+ '<strong>' + fmtVal(s.saddr) + '</strong>'
|
||||
+ (s.desc ? ' <span style="color:#999">' + fmtVal(s.desc) + '</span>' : '')
|
||||
+ '<span style="float:right;color:#2196F3;font-weight:700">' + (s.val !== undefined ? fmtVal(s.val) : '-') + '</span>'
|
||||
+ '</div>';
|
||||
}
|
||||
html += '</div><button class="btn btn-outline" id="plc-modal-cancel">取消</button></div></div>';
|
||||
var div = document.createElement('div');
|
||||
div.innerHTML = html;
|
||||
document.body.appendChild(div.firstElementChild);
|
||||
document.getElementById('plc-modal-cancel').onclick = closeModal;
|
||||
var rows = document.querySelectorAll('.plc-sig-row');
|
||||
for (var j = 0; j < rows.length; j++) {
|
||||
rows[j].onclick = function() {
|
||||
var saddr = this.getAttribute('data-saddr');
|
||||
var desc = this.getAttribute('data-desc') || '';
|
||||
closeModal();
|
||||
if (confirm('确定绑定信号 ' + saddr + ' 吗?')) { callback(saddr, desc); }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function closeModal() { var m = document.getElementById('plc-modal'); if (m) m.remove(); }
|
||||
|
||||
function refreshOutData() {
|
||||
var sigs = getOutSignals();
|
||||
if (Array.isArray(sigs)) outSignals = sigs;
|
||||
for (var i = 0; i < PlcDebugLeds.length; i++) {
|
||||
var found = findSignal(PlcDebugLeds[i].saddr);
|
||||
PlcDebugLeds[i].val = found ? found.val : 0;
|
||||
}
|
||||
for (var j = 0; j < PlcDebugButtons.length; j++) {
|
||||
var found2 = findSignal(PlcDebugButtons[j].saddr);
|
||||
PlcDebugButtons[j].val = found2 ? found2.val : 0;
|
||||
}
|
||||
}
|
||||
|
||||
function findSignal(saddr) {
|
||||
for (var i = 0; i < outSignals.length; i++) {
|
||||
if (outSignals[i].saddr === saddr) return outSignals[i];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function countChecked(obj) { var n = 0; for (var k in obj) { if (obj.hasOwnProperty(k)) n++; } return n; }
|
||||
|
||||
function updateDelBtn() {
|
||||
var ledCnt = countChecked(ledChecked);
|
||||
var btnCnt = countChecked(btnChecked);
|
||||
var total = ledCnt + btnCnt;
|
||||
var el = document.getElementById('plc-del-btn');
|
||||
if (el) {
|
||||
el.textContent = '🗑 删除选中' + (total > 0 ? '(' + total + ')' : '');
|
||||
el.disabled = (total === 0);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 增量刷新: 只更新 val 文本, 不重建 DOM (add/del 时才全重建) ---- */
|
||||
function incrRefresh() {
|
||||
refreshOutData();
|
||||
// 指示灯: 逐个更新 val 文本和样式
|
||||
for (var i = 0; i < PlcDebugLeds.length; i++) {
|
||||
var valEl = document.getElementById('led-val-' + i);
|
||||
var iconEl = document.getElementById('led-icon-' + i);
|
||||
var on = (parseFloat(PlcDebugLeds[i].val) > 0);
|
||||
if (valEl) valEl.textContent = '(' + PlcDebugLeds[i].val + ')';
|
||||
if (valEl) valEl.style.color = on ? '#4CAF50' : '#999';
|
||||
if (iconEl) { iconEl.className = 'plc-led' + (on ? ' plc-led-on' : ''); }
|
||||
}
|
||||
// 按键: 逐个更新 val 文本
|
||||
for (var j = 0; j < PlcDebugButtons.length; j++) {
|
||||
var bValEl = document.getElementById('btn-val-' + j);
|
||||
if (bValEl) bValEl.textContent = '(' + PlcDebugButtons[j].val + ')';
|
||||
}
|
||||
}
|
||||
|
||||
function renderAll() {
|
||||
refreshOutData();
|
||||
var html = '';
|
||||
html += '<div class="plc-toolbar">'
|
||||
+ '<button class="btn btn-warn" id="plc-manual-set">⚙ 手动置数</button>'
|
||||
+ '<button class="btn btn-outline" id="plc-refresh">🔄 刷新</button>'
|
||||
+ '<span style="flex:1"></span>'
|
||||
+ '<button class="btn btn-danger" id="plc-del-btn" disabled>🗑 删除选中</button>'
|
||||
+ '<button class="btn btn-outline" id="plc-exit-set">🚪 退出置数</button>'
|
||||
+ '</div>';
|
||||
html += '<div class="plc-actions">'
|
||||
+ '<button class="btn btn-primary" id="plc-add-led">💡 增加指示灯</button>'
|
||||
+ '<span style="flex:1"></span>'
|
||||
+ '<button class="btn btn-info" id="plc-add-btn">🔘 增加按键</button>'
|
||||
+ '</div>';
|
||||
html += '<div class="plc-grid">';
|
||||
|
||||
// 指示灯
|
||||
html += '<div class="plc-col"><h3>💡 指示灯</h3>';
|
||||
for (var i = 0; i < PlcDebugLeds.length; i++) {
|
||||
var led = PlcDebugLeds[i];
|
||||
var on = (parseFloat(led.val) > 0);
|
||||
var chk = ledChecked[i] ? ' checked' : '';
|
||||
html += '<div class="plc-item">'
|
||||
+ '<input type="checkbox" class="plc-cb" data-led-idx="' + i + '"' + chk + '>'
|
||||
+ '<span class="plc-led ' + (on ? 'plc-led-on' : '') + '" id="led-icon-' + i + '" data-led-idx="' + i + '">💡</span>'
|
||||
+ '<span class="plc-info">'
|
||||
+ (led.saddr ? '<span class="plc-saddr">' + fmtVal(led.saddr) + ' <span id="led-val-' + i + '" style="color:' + (on ? '#4CAF50' : '#999') + '">(' + fmtVal(led.val) + ')</span>'
|
||||
+ (led.desc ? ' <span style="color:#888;font-size:12px">' + fmtVal(led.desc) + '</span>' : '')
|
||||
: '<span style="color:#999">未绑定</span>')
|
||||
+ '</span></div>';
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
// 按键
|
||||
html += '<div class="plc-col"><h3>🔘 按键</h3>';
|
||||
for (var j = 0; j < PlcDebugButtons.length; j++) {
|
||||
var btn = PlcDebugButtons[j];
|
||||
var chk2 = btnChecked[j] ? ' checked' : '';
|
||||
html += '<div class="plc-item">'
|
||||
+ '<input type="checkbox" class="plc-cb" data-btn-idx="' + j + '"' + chk2 + '>'
|
||||
+ '<span class="plc-btn-icon" id="btn-icon-' + j + '" data-btn-idx="' + j + '">🔘</span>'
|
||||
+ '<span class="plc-info">'
|
||||
+ (btn.saddr ? '<span class="plc-saddr">' + fmtVal(btn.saddr) + ' <span id="btn-val-' + j + '" style="color:#2196F3">(' + fmtVal(btn.val) + ')</span>'
|
||||
+ (btn.desc ? ' <span style="color:#888;font-size:12px">' + fmtVal(btn.desc) + '</span>' : '')
|
||||
: '<span style="color:#999">未绑定</span>')
|
||||
+ '</span></div>';
|
||||
}
|
||||
html += '</div></div>';
|
||||
|
||||
document.getElementById('plc-content').innerHTML = html;
|
||||
|
||||
// ---- 事件绑定 ----
|
||||
document.getElementById('plc-add-led').onclick = function() {
|
||||
PlcDebugLeds.push({ saddr: '', desc: '', val: 0 });
|
||||
renderAll();
|
||||
};
|
||||
document.getElementById('plc-add-btn').onclick = function() {
|
||||
PlcDebugButtons.push({ saddr: '', desc: '', val: 0 });
|
||||
renderAll();
|
||||
};
|
||||
|
||||
// 删除选中
|
||||
document.getElementById('plc-del-btn').onclick = function() {
|
||||
var delLeds = [], delBtns = [];
|
||||
for (var k in ledChecked) { if (ledChecked.hasOwnProperty(k)) delLeds.push(parseInt(k)); }
|
||||
for (var k in btnChecked) { if (btnChecked.hasOwnProperty(k)) delBtns.push(parseInt(k)); }
|
||||
var total = delLeds.length + delBtns.length;
|
||||
if (total === 0) return;
|
||||
if (!confirm('确定删除 ' + total + ' 个控件?')) return;
|
||||
delLeds.sort(function(a,b){return b-a;});
|
||||
delBtns.sort(function(a,b){return b-a;});
|
||||
for (var a = 0; a < delLeds.length; a++) PlcDebugLeds.splice(delLeds[a], 1);
|
||||
for (var b = 0; b < delBtns.length; b++) PlcDebugButtons.splice(delBtns[b], 1);
|
||||
ledChecked = {}; btnChecked = {};
|
||||
renderAll();
|
||||
};
|
||||
|
||||
// LED 点击
|
||||
var ledIcons = document.querySelectorAll('.plc-led');
|
||||
for (var k = 0; k < ledIcons.length; k++) {
|
||||
ledIcons[k].onclick = function() {
|
||||
var idx = parseInt(this.getAttribute('data-led-idx'));
|
||||
if (PlcDebugLeds[idx].saddr) {
|
||||
if (!confirm('当前绑定: ' + PlcDebugLeds[idx].saddr + ', 是否修改绑定?')) return;
|
||||
}
|
||||
showSignalPicker(function(saddr, desc) {
|
||||
PlcDebugLeds[idx].saddr = saddr;
|
||||
PlcDebugLeds[idx].desc = desc || '';
|
||||
renderAll();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// 按钮点击
|
||||
var btnIcons = document.querySelectorAll('.plc-btn-icon');
|
||||
for (var m = 0; m < btnIcons.length; m++) {
|
||||
btnIcons[m].onclick = function() {
|
||||
var idx = parseInt(this.getAttribute('data-btn-idx'));
|
||||
if (!PlcDebugButtons[idx].saddr) {
|
||||
showSignalPicker(function(saddr, desc) {
|
||||
PlcDebugButtons[idx].saddr = saddr;
|
||||
PlcDebugButtons[idx].desc = desc || '';
|
||||
PlcDebugButtons[idx].val = 0;
|
||||
renderAll();
|
||||
});
|
||||
return;
|
||||
}
|
||||
var newVal = (PlcDebugButtons[idx].val == 1) ? 0 : 1;
|
||||
PlcDebugButtons[idx].val = newVal;
|
||||
WsClient.send({ curd:'set', signal_type:'out', saddr:PlcDebugButtons[idx].saddr, signal_data:String(newVal), setting_zone:'0' });
|
||||
showToast('✅ 已发送翻转: ' + PlcDebugButtons[idx].saddr + ' → ' + newVal);
|
||||
incrRefresh();
|
||||
};
|
||||
}
|
||||
|
||||
// 勾选框
|
||||
var allCbs = document.querySelectorAll('.plc-cb');
|
||||
for (var n = 0; n < allCbs.length; n++) {
|
||||
allCbs[n].onclick = function(e) {
|
||||
e.stopPropagation();
|
||||
var ledIdx = this.getAttribute('data-led-idx');
|
||||
var btnIdx = this.getAttribute('data-btn-idx');
|
||||
if (ledIdx !== null) {
|
||||
if (this.checked) ledChecked[ledIdx] = true; else delete ledChecked[ledIdx];
|
||||
} else if (btnIdx !== null) {
|
||||
if (this.checked) btnChecked[btnIdx] = true; else delete btnChecked[btnIdx];
|
||||
}
|
||||
updateDelBtn();
|
||||
};
|
||||
}
|
||||
|
||||
document.getElementById('plc-refresh').onclick = function() {
|
||||
refreshOutData();
|
||||
renderAll();
|
||||
showToast('🔄 已刷新');
|
||||
};
|
||||
var manualBtn = document.getElementById('plc-manual-set');
|
||||
var exitBtn = document.getElementById('plc-exit-set');
|
||||
if (manualBtn) manualBtn.onclick = function() { alert('手动置数功能待实现'); };
|
||||
if (exitBtn) exitBtn.onclick = function() { alert('退出置数功能待实现'); };
|
||||
}
|
||||
|
||||
function showToast(msg) {
|
||||
var t = document.getElementById('plc-toast');
|
||||
if (!t) {
|
||||
t = document.createElement('div');
|
||||
t.id = 'plc-toast';
|
||||
t.style.cssText = 'position:fixed;top:80px;left:50%;transform:translateX(-50%);background:#333;color:#fff;padding:10px 24px;border-radius:6px;z-index:9999;font-size:14px;box-shadow:0 4px 12px rgba(0,0,0,0.3);transition:opacity 0.3s;pointer-events:none';
|
||||
document.body.appendChild(t);
|
||||
}
|
||||
t.textContent = msg; t.style.opacity = '1';
|
||||
clearTimeout(t._timer);
|
||||
t._timer = setTimeout(function() { t.style.opacity = '0'; }, 1200);
|
||||
}
|
||||
|
||||
return {
|
||||
render: function(container) {
|
||||
ledChecked = {}; btnChecked = {};
|
||||
container.innerHTML = ''
|
||||
+ '<div class="page-header"><h2>PLC 调试</h2><p>指示灯绑定 out 已注册信号(>0 亮/≤0 灭),按键绑定 out 已注册信号(点击翻转 0↔1)</p></div>'
|
||||
+ '<div id="plc-content"></div>';
|
||||
renderAll();
|
||||
},
|
||||
onEnter: function() { refreshOutData(); renderAll(); },
|
||||
onLeave: function() {},
|
||||
onData: function(data) {
|
||||
if (Array.isArray(data)) { outSignals = data; }
|
||||
incrRefresh();
|
||||
}
|
||||
};
|
||||
})();
|
||||
Loading…
Reference in New Issue