<修改> 1、消除cmd中的编译告警;2、消除log中的编译告警;3、增加modbus客户端程序;4、webserver增加导入导出信号表功能,可以导出已增加的信号,也可以一键导入想要观察的信号
This commit is contained in:
parent
c6501da92f
commit
19a2f2a3c0
|
|
@ -0,0 +1,107 @@
|
||||||
|
# 前端信号批量导出/导入 XML 功能
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
在 5 个信号管理页面(out/in/yk/ao/param)的工具栏中新增"导出"和"导入"按钮,实现信号的批量导出到 XML 文件和从 XML 文件批量导入。
|
||||||
|
|
||||||
|
## 功能描述
|
||||||
|
|
||||||
|
- **导出**:将当前页面已添加的信号列表生成为 XML 文件,通过浏览器下载
|
||||||
|
- **导入**:读取本地 XML 文件,解析后按 seq 序号顺序逐个执行 add 操作
|
||||||
|
|
||||||
|
## 涉及文件
|
||||||
|
|
||||||
|
| 文件 | 修改类型 | 说明 |
|
||||||
|
|------|---------|------|
|
||||||
|
| `test/web_root/js/pages.js` | 修改 | 主要逻辑变更 |
|
||||||
|
| `src/system/libweb_server/src/packed_fs.c` | 重新生成 | `pack_web.sh` 打包输出 |
|
||||||
|
|
||||||
|
## 代码变更
|
||||||
|
|
||||||
|
### 1. 新增函数 `escapeXml(str)`
|
||||||
|
|
||||||
|
对 XML 特殊字符进行转义(`& < > " '`),用于拼接 XML 字符串时防止格式损坏。
|
||||||
|
|
||||||
|
### 2. 新增函数 `exportToXml(signalType, data)`
|
||||||
|
|
||||||
|
- 参数:`signalType` — 信号类型字符串,`data` — 信号数组
|
||||||
|
- 逻辑:
|
||||||
|
1. 空数据检查 → 弹窗提示
|
||||||
|
2. 拼接 XML 字符串(`<?xml ...?>` + `<signals type="...">` + 逐个 signal 节点)
|
||||||
|
3. 输出字段:`saddr`、`desc`、`data_type`、`val`(全部 escapeXml 转义)
|
||||||
|
4. 创建 Blob → URL.createObjectURL → `<a download>` 触发下载
|
||||||
|
5. 文件名格式:`signals_{type}_{timestamp}.xml`
|
||||||
|
6. 清理:removeChild + revokeObjectURL
|
||||||
|
|
||||||
|
### 3. 新增函数 `importFromXml(signalType)`
|
||||||
|
|
||||||
|
- 参数:`signalType` — 当前页面信号类型
|
||||||
|
- 逻辑:
|
||||||
|
1. 创建隐藏 `<input type="file" accept=".xml">` 并触发点击
|
||||||
|
2. 文件选中后用 FileReader 读取文本
|
||||||
|
3. DOMParser 解析 XML
|
||||||
|
4. 校验:根元素必为 `<signals>`,`type` 属性必须与 `signalType` 一致
|
||||||
|
5. 提取所有 `<signal>` 节点的 `<saddr>`,按 `seq` 属性升序排序
|
||||||
|
6. confirm 确认后,逐个发送 WsClient.send add 命令
|
||||||
|
7. 完成后 alert 提示结果
|
||||||
|
|
||||||
|
### 4. 修改函数 `makeToolbar(signalType, showSet)`
|
||||||
|
|
||||||
|
在工具栏末尾添加两个按钮:
|
||||||
|
- `<span style="flex:1"></span>` — 弹性撑开
|
||||||
|
- `<button class="btn btn-info" id="tb-export">📤 导出</button>`
|
||||||
|
- `<button class="btn btn-warn" id="tb-import">📥 导入</button>`
|
||||||
|
|
||||||
|
### 5. 修改函数 `bindToolbar(signalType, getDataFn)`
|
||||||
|
|
||||||
|
- 参数变更:第二个参数从 data 数组改为 getter 函数 `getDataFn`
|
||||||
|
- 导出按钮:`onclick` 调用 `getDataFn()` 获取最新数据,传给 `exportToXml`
|
||||||
|
- 导入按钮:`onclick` 直接调用 `importFromXml(signalType)`
|
||||||
|
|
||||||
|
### 6. 修改各页面调用
|
||||||
|
|
||||||
|
5 个页面均传递 getter 函数解决数据引用过时问题:
|
||||||
|
```javascript
|
||||||
|
bindToolbar('out', function() { return data; });
|
||||||
|
```
|
||||||
|
|
||||||
|
## XML 格式
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<signals type="out">
|
||||||
|
<signal seq="1">
|
||||||
|
<saddr>iec.run_cnt</saddr>
|
||||||
|
<desc>运行计数</desc>
|
||||||
|
<data_type>INT32</data_type>
|
||||||
|
<val>0</val>
|
||||||
|
</signal>
|
||||||
|
</signals>
|
||||||
|
```
|
||||||
|
|
||||||
|
- 根元素 `type` 标记信号类型,导入时校验是否匹配当前页面
|
||||||
|
- `desc`、`data_type`、`val` 为参考字段,导入时忽略(仅使用 `saddr`)
|
||||||
|
|
||||||
|
## 构建注意事项
|
||||||
|
|
||||||
|
Web 服务使用 `packed_fs.c`(通过 `release/pack_web.sh` 将 `test/web_root/` 打包为 C 字节数组)。修改前端文件后需要:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd release && bash pack_web.sh # 重新打包
|
||||||
|
rm -f x86/lib/libweb_server.a # 删除旧库
|
||||||
|
make -C src/system/libweb_server # 重新编译 web_server 库
|
||||||
|
make # 重新链接 RTU
|
||||||
|
cp x86/exe/RTU ../test/ # 复制到运行目录
|
||||||
|
```
|
||||||
|
|
||||||
|
## 验证结果
|
||||||
|
|
||||||
|
| 验证项 | 结果 |
|
||||||
|
|--------|------|
|
||||||
|
| 5 个页面均有导出/导入按钮 | ✅ |
|
||||||
|
| 导出按钮响应点击(空数据提示) | ✅ |
|
||||||
|
| 导出按钮触发文件下载(有数据时) | ✅(代码审查确认) |
|
||||||
|
| 导入按钮弹出文件选择对话框 | ✅ |
|
||||||
|
| XML 解析逻辑正确 | ✅(代码审查确认) |
|
||||||
|
| 发送 add 命令逻辑正确 | ✅(代码审查确认) |
|
||||||
|
| 编译通过 | ✅ |
|
||||||
|
|
@ -21,3 +21,4 @@ APP_MODULE(IEC, iec, app_iec_init1, app_iec_init2,
|
||||||
APP_MODULE(SELF_PTL, self_ptl, app_self_ptl_init1, app_self_ptl_init2, app_self_ptl )
|
APP_MODULE(SELF_PTL, self_ptl, app_self_ptl_init1, app_self_ptl_init2, app_self_ptl )
|
||||||
APP_MODULE(IEC61850M, iec61850m, app_iec61850m_init1, app_iec61850m_init2, app_iec61850m )
|
APP_MODULE(IEC61850M, iec61850m, app_iec61850m_init1, app_iec61850m_init2, app_iec61850m )
|
||||||
APP_MODULE(IEC61850S, iec61850s, app_iec61850s_init1, app_iec61850s_init2, app_iec61850s )
|
APP_MODULE(IEC61850S, iec61850s, app_iec61850s_init1, app_iec61850s_init2, app_iec61850s )
|
||||||
|
APP_MODULE(MODBUS_M, modbus_m, app_modbus_m_init1, app_modbus_m_init2, app_modbus_m )
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,8 @@ MakeDirCommand := mkdir -p
|
||||||
# 静态库链接(注意顺序很重要,被依赖的库放在后面)
|
# 静态库链接(注意顺序很重要,被依赖的库放在后面)
|
||||||
# Libs := -lcom_channel -lcom_scan -liec -lself_ptl -lfunc -ltask -lcomm -lshell -lpthread
|
# Libs := -lcom_channel -lcom_scan -liec -lself_ptl -lfunc -ltask -lcomm -lshell -lpthread
|
||||||
|
|
||||||
Libs := -lcom_decode -liec -liec61850m -liec61850s -lself_ptl -lweb_server -ldatacenter
|
Libs := -lcom_decode -liec -liec61850m -liec61850s -lmodbus_m -lself_ptl -lweb_server -ldatacenter
|
||||||
Libs += -l60870 -licp67 -lmms_m -lmms_s -lmongoose
|
Libs += -l60870 -licp67 -lmms_m -lmms_s -lmongoose -lmodbus
|
||||||
Libs += -liec61850
|
Libs += -liec61850
|
||||||
Libs += -lmy_xxhash -lcmd -lmd5 -lxml -lcJSON -lcomm -ltask -lfunc
|
Libs += -lmy_xxhash -lcmd -lmd5 -lxml -lcJSON -lcomm -ltask -lfunc
|
||||||
Libs += -lpthread
|
Libs += -lpthread
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
# 包含外部Makefile
|
||||||
|
include ./../../../linux.mk
|
||||||
|
|
||||||
|
ProjectName := libmodbus_m
|
||||||
|
|
||||||
|
DIR := $(realpath $(CURDIR)/..)
|
||||||
|
LAST_DIR := $(notdir $(DIR))
|
||||||
|
|
||||||
|
|
||||||
|
# 静态库文件目标
|
||||||
|
OutputFile := $(LIB_REL)/$(ProjectName).a
|
||||||
|
ArchiveCommand := ar rcs
|
||||||
|
|
||||||
|
MakeDirCommand := mkdir -p
|
||||||
|
|
||||||
|
OBJ := $(DIR)/$(ProjectName)/obj
|
||||||
|
SRC := $(SRC_ROOT_DIR)/$(LAST_DIR)/$(ProjectName)/src
|
||||||
|
INC := $(SRC_ROOT_DIR)/$(LAST_DIR)/$(ProjectName)/inc
|
||||||
|
INC += $(REL_INC)
|
||||||
|
|
||||||
|
# 定义源文件和对象文件
|
||||||
|
SOURCES := $(wildcard $(SRC)/*.c) $(wildcard $(SRC)/*.cpp)
|
||||||
|
OBJECTS := $(patsubst $(SRC)/%.c, $(OBJ)/%.o,$(filter %.c,$(SOURCES)))
|
||||||
|
OBJECTSCPP := $(patsubst $(SRC)/%.cpp, $(OBJ)/%.o,$(filter %.cpp,$(SOURCES)))
|
||||||
|
|
||||||
|
ALL_OBJECTS := $(OBJECTS) $(OBJECTSCPP)
|
||||||
|
|
||||||
|
# 编译选项
|
||||||
|
APP_C_FLAGS := $(C_FLAGS)
|
||||||
|
APP_C_FLAGS += -I $(INC) -g
|
||||||
|
|
||||||
|
APP_C_FLAGS += -O0
|
||||||
|
|
||||||
|
# 伪目标
|
||||||
|
.PHONY : all clean veryclean rebuild
|
||||||
|
|
||||||
|
# 默认目标
|
||||||
|
all: $(OutputFile)
|
||||||
|
|
||||||
|
# 构建静态库
|
||||||
|
$(OutputFile): $(ALL_OBJECTS)
|
||||||
|
@$(MakeDirCommand) $(dir $@)
|
||||||
|
$(ArchiveCommand) $@ $^
|
||||||
|
@echo "Static library built: $@"
|
||||||
|
|
||||||
|
# 重新构建
|
||||||
|
rebuild: veryclean all
|
||||||
|
|
||||||
|
# 清理
|
||||||
|
clean:
|
||||||
|
rm -f $(OBJ)/*.o
|
||||||
|
rm -f $(OBJ)/*.d
|
||||||
|
rm -f $(OutputFile)
|
||||||
|
rm -f ./out/*.*
|
||||||
|
|
||||||
|
# 非常彻底的清理
|
||||||
|
veryclean: clean
|
||||||
|
rm -f ./out/$(OutputFile)
|
||||||
|
rm -f $(SRC)/*.bak
|
||||||
|
rm -f $(INC)/*.bak
|
||||||
|
|
||||||
|
# C对象文件规则
|
||||||
|
$(OBJECTS): $(OBJ)/%.o: $(SRC)/%.c
|
||||||
|
@mkdir -p $(dir $@)
|
||||||
|
$(CC) $(APP_C_FLAGS) -o $@ -c $<
|
||||||
|
|
||||||
|
# C++对象文件规则
|
||||||
|
$(OBJECTSCPP): $(OBJ)/%.o: $(SRC)/%.cpp
|
||||||
|
@mkdir -p $(dir $@)
|
||||||
|
$(CPP) $(APP_C_FLAGS) -o $@ -c $<
|
||||||
|
|
@ -9,6 +9,7 @@ SUBDIRS += ./libiec61850m
|
||||||
SUBDIRS += ./libiec61850s
|
SUBDIRS += ./libiec61850s
|
||||||
SUBDIRS += ./libself_ptl
|
SUBDIRS += ./libself_ptl
|
||||||
SUBDIRS += ./libweb_server
|
SUBDIRS += ./libweb_server
|
||||||
|
SUBDIRS += ./libmodbus_m
|
||||||
|
|
||||||
|
|
||||||
SUBDIRS += ./RTU
|
SUBDIRS += ./RTU
|
||||||
|
|
|
||||||
|
|
@ -789,6 +789,12 @@ modbus_t *modbus_new_tcp(const char *ip, int port)
|
||||||
size_t dest_size;
|
size_t dest_size;
|
||||||
size_t ret_size;
|
size_t ret_size;
|
||||||
|
|
||||||
|
ctx = (modbus_t *) malloc(sizeof(modbus_t));
|
||||||
|
if (ctx == NULL) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
_modbus_init_common(ctx);
|
||||||
ctx->backend = &_modbus_tcp_backend;
|
ctx->backend = &_modbus_tcp_backend;
|
||||||
|
|
||||||
ctx->backend_data = (modbus_tcp_t *) malloc(sizeof(modbus_tcp_t));
|
ctx->backend_data = (modbus_tcp_t *) malloc(sizeof(modbus_tcp_t));
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
#include <signal.h> // 加这个
|
#include <signal.h> // 加这个
|
||||||
|
|
||||||
// 修复 write 警告
|
// 修复 write 警告
|
||||||
#define SAFE_WRITE(fd, buf, sz) do { (void)write(fd, buf, sz); } while(0)
|
#define SAFE_WRITE(fd, buf, sz) do { ssize_t ___r = write(fd, buf, sz); (void)___r; } while(0)
|
||||||
|
|
||||||
// 静态 vector:所有命令自动存在这里
|
// 静态 vector:所有命令自动存在这里
|
||||||
static std::vector<stru_cmd>& get_cmd_list()
|
static std::vector<stru_cmd>& get_cmd_list()
|
||||||
|
|
|
||||||
|
|
@ -286,8 +286,8 @@ static int init_log_directory(void)
|
||||||
// 轮转日志文件
|
// 轮转日志文件
|
||||||
static void rotate_log_files(void)
|
static void rotate_log_files(void)
|
||||||
{
|
{
|
||||||
char old_path[256];
|
char old_path[512];
|
||||||
char new_path[256];
|
char new_path[512];
|
||||||
|
|
||||||
pthread_mutex_lock(&logger.file_mutex);
|
pthread_mutex_lock(&logger.file_mutex);
|
||||||
|
|
||||||
|
|
@ -327,7 +327,7 @@ static FILE* open_log_file_internal(void)
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
char file_path[256];
|
char file_path[512];
|
||||||
get_log_file_path(file_path, sizeof(file_path), 0);
|
get_log_file_path(file_path, sizeof(file_path), 0);
|
||||||
|
|
||||||
// 检查文件大小
|
// 检查文件大小
|
||||||
|
|
|
||||||
|
|
@ -113,9 +113,6 @@ LOCAL int app_config_load()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
int app_sys_init1(void *arg);
|
|
||||||
int app_sys_init2(void *arg);
|
|
||||||
void *app_sys(void *arg);
|
|
||||||
LOCAL std::vector<stru_app_msg_queue> g_vec_app_msg_queue =
|
LOCAL std::vector<stru_app_msg_queue> g_vec_app_msg_queue =
|
||||||
{
|
{
|
||||||
// 消息队列注册表 — 由 app_msg_queue.h X-macro 自动生成
|
// 消息队列注册表 — 由 app_msg_queue.h X-macro 自动生成
|
||||||
|
|
@ -208,6 +205,11 @@ LOCAL int app_msg_queue_init()
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LOCAL bool is_app_enabled(const char *name)
|
||||||
|
{
|
||||||
|
return g_enabled_apps.find(name) != g_enabled_apps.end();
|
||||||
|
}
|
||||||
|
|
||||||
LOCAL int app_init()
|
LOCAL int app_init()
|
||||||
{
|
{
|
||||||
uint32_t i = 0;
|
uint32_t i = 0;
|
||||||
|
|
@ -220,6 +222,12 @@ LOCAL int app_init()
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(!is_app_enabled(p->name))
|
||||||
|
{
|
||||||
|
MY_LOG_E("app skipped (disabled by config): %s", p->name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if(0 != p->app_init1((void *)p))
|
if(0 != p->app_init1((void *)p))
|
||||||
{
|
{
|
||||||
MY_LOG_E("app_init1 failed, name:%s", p->name);
|
MY_LOG_E("app_init1 failed, name:%s", p->name);
|
||||||
|
|
@ -235,6 +243,12 @@ LOCAL int app_init()
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(!is_app_enabled(p->name))
|
||||||
|
{
|
||||||
|
MY_LOG_E("app skipped (disabled by config): %s", p->name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if(0 != p->app_init2((void *)p))
|
if(0 != p->app_init2((void *)p))
|
||||||
{
|
{
|
||||||
MY_LOG_E("app_init2 failed, name:%s", p->name);
|
MY_LOG_E("app_init2 failed, name:%s", p->name);
|
||||||
|
|
@ -264,9 +278,9 @@ LOCAL int app_init()
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(g_enabled_apps.find(p->name) == g_enabled_apps.end())
|
if(!is_app_enabled(p->name))
|
||||||
{
|
{
|
||||||
MY_LOG_I("app skipped (disabled by config): %s", p->name);
|
MY_LOG_E("app skipped (disabled by config): %s", p->name);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "myBase.h"
|
||||||
|
#include "mySystem.h"
|
||||||
|
#include "myModbus.h"
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
#include <map>
|
||||||
|
|
||||||
|
#define MODULE_MODBUS_M "modbus_m"
|
||||||
|
|
||||||
|
#define MODBUS_M_STR_LEN 64
|
||||||
|
#define MODBUS_M_MAX_SLAVE_NUM 16
|
||||||
|
|
||||||
|
// 通讯通道配置
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
uint32_t id; // 通道ID
|
||||||
|
std::string mode; // "tcp" 或 "rtu"
|
||||||
|
std::string host; // TCP 目标IP
|
||||||
|
uint16_t port; // TCP 目标端口
|
||||||
|
std::string device; // RTU 串口设备
|
||||||
|
int baud; // RTU 波特率
|
||||||
|
char parity; // RTU 校验位
|
||||||
|
int data_bit; // RTU 数据位
|
||||||
|
int stop_bit; // RTU 停止位
|
||||||
|
int timeout_ms; // 请求超时
|
||||||
|
int retry_cnt; // 失败重试次数
|
||||||
|
modbus_t *ctx; // libmodbus 上下文
|
||||||
|
bool connected; // 连接状态
|
||||||
|
}stru_modbus_m_channel;
|
||||||
|
|
||||||
|
// 点表条目
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
uint16_t no; // 序号
|
||||||
|
uint8_t type; // 数据类型: 2=bit, 4=uint16, 6=float32, 7=uint32, 13=string
|
||||||
|
uint32_t channel; // 归属通道ID
|
||||||
|
uint8_t slave; // 从站地址
|
||||||
|
uint8_t fc; // 功能码
|
||||||
|
uint16_t addr; // 寄存器/线圈起始地址
|
||||||
|
uint16_t count; // 连续读取数量
|
||||||
|
uint8_t bit; // 位位置(0-15),type=2且fc=03/04时有效
|
||||||
|
float factor; // 遥测系数
|
||||||
|
float offset; // 遥测偏移
|
||||||
|
std::string saddr; // 内部信号地址
|
||||||
|
std::string desc; // 描述
|
||||||
|
void *p_val; // 数据指针
|
||||||
|
}stru_modbus_m_item;
|
||||||
|
|
||||||
|
// 点表页
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
std::string desc;
|
||||||
|
std::vector<stru_modbus_m_item> st_vec; // 遥信
|
||||||
|
std::vector<stru_modbus_m_item> mx_vec; // 遥测
|
||||||
|
std::vector<stru_modbus_m_item> co_vec; // 遥控
|
||||||
|
std::vector<stru_modbus_m_item> ao_vec; // 参数
|
||||||
|
}stru_modbus_m_point;
|
||||||
|
|
||||||
|
// 写请求(用于 Co/Ao 异步写入)
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
uint32_t channel;
|
||||||
|
uint8_t slave;
|
||||||
|
uint8_t fc;
|
||||||
|
uint16_t addr;
|
||||||
|
uint16_t value;
|
||||||
|
bool is_coil; // true=写线圈, false=写寄存器
|
||||||
|
}stru_modbus_m_write_req;
|
||||||
|
|
||||||
|
// 模块主配置
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
std::vector<stru_modbus_m_channel> channels;
|
||||||
|
stru_modbus_m_point point;
|
||||||
|
}stru_modbus_m_cfg;
|
||||||
|
|
||||||
|
|
||||||
|
// API 声明
|
||||||
|
int modbus_m_init();
|
||||||
|
int modbus_m_cfg_parse(const std::string &path);
|
||||||
|
void modbus_m_show_info();
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,112 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
Modbus 主站 (Master) 配置文件
|
||||||
|
支持多通道(TCP/RTU)、总线多设备、FC03/04 寄存器读线圈
|
||||||
|
-->
|
||||||
|
<Root>
|
||||||
|
<!-- ============================================================
|
||||||
|
通道定义 — 支持多个 TCP 或 RTU 通道
|
||||||
|
每个通道独立连接参数,通道之间互不影响
|
||||||
|
|
||||||
|
参数说明:
|
||||||
|
id — 通道唯一标识 (0~N)
|
||||||
|
mode — "tcp" 或 "rtu"
|
||||||
|
host — TCP 目标 IP
|
||||||
|
port — TCP 目标端口 (默认 502)
|
||||||
|
device — RTU 串口设备路径
|
||||||
|
baud — RTU 波特率
|
||||||
|
parity — RTU 校验位 (N/E/O)
|
||||||
|
data_bit — RTU 数据位
|
||||||
|
stop_bit — RTU 停止位
|
||||||
|
timeout_ms — 请求超时 (ms)
|
||||||
|
retry_cnt — 失败重试次数
|
||||||
|
============================================================ -->
|
||||||
|
<Channel id="0" desc="通道0-TCP设备1" mode="tcp" host="192.168.1.100" port="502" timeout_ms="1000" retry_cnt="3" />
|
||||||
|
<Channel id="1" desc="通道1-TCP设备2" mode="tcp" host="192.168.1.101" port="502" timeout_ms="1000" retry_cnt="3" />
|
||||||
|
<Channel id="2" desc="通道2-RTU总线1" mode="rtu" device="/dev/ttyUSB0" baud="115200" parity="N" data_bit="8" stop_bit="1" timeout_ms="1000" retry_cnt="3" />
|
||||||
|
<Channel id="3" desc="通道3-RTU总线2" mode="rtu" device="/dev/ttyUSB1" baud="115200" parity="N" data_bit="8" stop_bit="1" timeout_ms="1000" retry_cnt="3" />
|
||||||
|
|
||||||
|
<!-- ============================================================
|
||||||
|
点表定义
|
||||||
|
|
||||||
|
通用属性:
|
||||||
|
no — 信号序号 (全局唯一)
|
||||||
|
type — 数据类型: 2=bit(开关量), 4=uint16, 6=float32, 7=uint32, 13=string
|
||||||
|
channel — 归属通道 ID (对应上方 Channel id)
|
||||||
|
slave — 从站地址 (1~247)
|
||||||
|
fc — Modbus 功能码:
|
||||||
|
0x01=读线圈, 0x02=读离散输入,
|
||||||
|
0x03=读保持寄存器, 0x04=读输入寄存器,
|
||||||
|
0x05=写单线圈, 0x06=写单寄存器
|
||||||
|
0x0F=写多线圈, 0x10=写多寄存器
|
||||||
|
addr — 寄存器/线圈起始地址 (0-based)
|
||||||
|
count — 连续读取数量
|
||||||
|
bit — 位位置 (0~15), 仅当 type=2 且 fc=03/04 时有效
|
||||||
|
factor — 遥测系数
|
||||||
|
offset — 遥测偏移
|
||||||
|
saddr — 内部信号地址 (数据中心关联用)
|
||||||
|
desc — 中文描述
|
||||||
|
|
||||||
|
重要: FC03/04 + type=2 表示"从寄存器中提取线圈值",
|
||||||
|
即一个寄存器的 16 个 bit 对应 16 个线圈。
|
||||||
|
bit 属性指定提取哪个位 (0~15)。
|
||||||
|
============================================================ -->
|
||||||
|
<Point desc="点表">
|
||||||
|
<!-- ========== 遥信 (St) ========== -->
|
||||||
|
<St desc="遥信">
|
||||||
|
<!-- 通道0 - TCP设备1 - 读离散输入 (FC=0x02) -->
|
||||||
|
<Item no="0" type="2" channel="0" slave="1" fc="0x02" addr="0" count="1" saddr="modbus_m.ch0.slave1.st.0" desc="通道0-设备1-遥信01" />
|
||||||
|
<Item no="1" type="2" channel="0" slave="1" fc="0x02" addr="1" count="1" saddr="modbus_m.ch0.slave1.st.1" desc="通道0-设备1-遥信02" />
|
||||||
|
|
||||||
|
<!-- 通道1 - TCP设备2 - 读线圈 (FC=0x01) -->
|
||||||
|
<Item no="2" type="2" channel="1" slave="1" fc="0x01" addr="0" count="1" saddr="modbus_m.ch1.slave1.st.0" desc="通道1-设备1-遥信01" />
|
||||||
|
<Item no="3" type="2" channel="1" slave="1" fc="0x01" addr="1" count="1" saddr="modbus_m.ch1.slave1.st.1" desc="通道1-设备1-遥信02" />
|
||||||
|
|
||||||
|
<!-- 通道2 - RTU总线1 - 从站1/2 - 读离散输入 -->
|
||||||
|
<Item no="4" type="2" channel="2" slave="1" fc="0x02" addr="0" count="1" saddr="modbus_m.ch2.slave1.st.0" desc="通道2-从站1-遥信01" />
|
||||||
|
<Item no="5" type="2" channel="2" slave="1" fc="0x02" addr="1" count="1" saddr="modbus_m.ch2.slave1.st.1" desc="通道2-从站1-遥信02" />
|
||||||
|
<Item no="6" type="2" channel="2" slave="2" fc="0x02" addr="0" count="1" saddr="modbus_m.ch2.slave2.st.0" desc="通道2-从站2-遥信01" />
|
||||||
|
|
||||||
|
<!-- 通道3 - RTU总线2 - 从站1 - 从寄存器读线圈 (FC=0x03 + type=2 + bit) -->
|
||||||
|
<Item no="7" type="2" channel="3" slave="1" fc="0x03" addr="100" count="1" bit="0" saddr="modbus_m.ch3.slave1.st.bit0" desc="通道3-从站1-寄存器100-bit0线圈" />
|
||||||
|
<Item no="8" type="2" channel="3" slave="1" fc="0x03" addr="100" count="1" bit="1" saddr="modbus_m.ch3.slave1.st.bit1" desc="通道3-从站1-寄存器100-bit1线圈" />
|
||||||
|
<Item no="9" type="2" channel="3" slave="1" fc="0x03" addr="100" count="1" bit="2" saddr="modbus_m.ch3.slave1.st.bit2" desc="通道3-从站1-寄存器100-bit2线圈" />
|
||||||
|
<Item no="10" type="2" channel="3" slave="2" fc="0x04" addr="200" count="1" bit="0" saddr="modbus_m.ch3.slave2.st.bit0" desc="通道3-从站2-输入寄存器200-bit0" />
|
||||||
|
</St>
|
||||||
|
|
||||||
|
<!-- ========== 遥测 (Mx) — 输入寄存器/保持寄存器 ========== -->
|
||||||
|
<Mx desc="遥测">
|
||||||
|
<!-- 通道0 - TCP设备1 - 读输入寄存器 (FC=0x04) -->
|
||||||
|
<Item no="0" type="6" channel="0" slave="1" fc="0x04" addr="0" count="2" saddr="modbus_m.ch0.slave1.mx.0" desc="通道0-电压A相" factor="0.01" offset="0" />
|
||||||
|
<Item no="1" type="6" channel="0" slave="1" fc="0x04" addr="2" count="2" saddr="modbus_m.ch0.slave1.mx.1" desc="通道0-电压B相" factor="0.01" offset="0" />
|
||||||
|
<Item no="2" type="7" channel="0" slave="1" fc="0x04" addr="4" count="2" saddr="modbus_m.ch0.slave1.mx.2" desc="通道0-电能" factor="1" offset="0" />
|
||||||
|
|
||||||
|
<!-- 通道1 - TCP设备2 - 读保持寄存器 (FC=0x03) -->
|
||||||
|
<Item no="3" type="6" channel="1" slave="1" fc="0x03" addr="0" count="2" saddr="modbus_m.ch1.slave1.mx.0" desc="通道1-电流A相" factor="0.001" offset="0" />
|
||||||
|
|
||||||
|
<!-- 通道2 - RTU总线1 - 从站1/2 -->
|
||||||
|
<Item no="4" type="6" channel="2" slave="1" fc="0x04" addr="0" count="2" saddr="modbus_m.ch2.slave1.mx.0" desc="通道2-从站1-电压" factor="0.01" offset="0" />
|
||||||
|
<Item no="5" type="6" channel="2" slave="2" fc="0x04" addr="0" count="2" saddr="modbus_m.ch2.slave2.mx.0" desc="通道2-从站2-电压" factor="0.01" offset="0" />
|
||||||
|
|
||||||
|
<!-- 通道3 - RTU总线2 - 从站1 -->
|
||||||
|
<Item no="6" type="4" channel="3" slave="1" fc="0x03" addr="0" count="1" saddr="modbus_m.ch3.slave1.mx.0" desc="通道3-从站1-温度" />
|
||||||
|
</Mx>
|
||||||
|
|
||||||
|
<!-- ========== 遥控 (Co) — 写线圈 ========== -->
|
||||||
|
<Co desc="遥控">
|
||||||
|
<Item no="0" type="2" channel="0" slave="1" fc="0x05" addr="0" count="1" saddr="modbus_m.ch0.slave1.co.0" desc="通道0-设备1-分闸" />
|
||||||
|
<Item no="1" type="2" channel="0" slave="1" fc="0x05" addr="1" count="1" saddr="modbus_m.ch0.slave1.co.1" desc="通道0-设备1-合闸" />
|
||||||
|
<Item no="2" type="2" channel="2" slave="1" fc="0x05" addr="0" count="1" saddr="modbus_m.ch2.slave1.co.0" desc="通道2-从站1-分闸" />
|
||||||
|
<Item no="3" type="2" channel="2" slave="2" fc="0x05" addr="0" count="1" saddr="modbus_m.ch2.slave2.co.0" desc="通道2-从站2-分闸" />
|
||||||
|
<Item no="4" type="2" channel="3" slave="1" fc="0x05" addr="0" count="1" saddr="modbus_m.ch3.slave1.co.0" desc="通道3-从站1-分闸" />
|
||||||
|
</Co>
|
||||||
|
|
||||||
|
<!-- ========== 参数 (Ao) — 保持寄存器 读写 ========== -->
|
||||||
|
<Ao desc="参数">
|
||||||
|
<Item no="0" type="4" channel="0" slave="1" fc="0x06" addr="100" count="1" saddr="modbus_m.ch0.slave1.ao.0" desc="通道0-设备1-通讯地址" />
|
||||||
|
<Item no="1" type="4" channel="0" slave="1" fc="0x06" addr="101" count="1" saddr="modbus_m.ch0.slave1.ao.1" desc="通道0-设备1-波特率" />
|
||||||
|
<Item no="2" type="4" channel="2" slave="1" fc="0x06" addr="100" count="1" saddr="modbus_m.ch2.slave1.ao.0" desc="通道2-从站1-通讯地址" />
|
||||||
|
<Item no="3" type="4" channel="3" slave="1" fc="0x06" addr="100" count="1" saddr="modbus_m.ch3.slave1.ao.0" desc="通道3-从站1-通讯地址" />
|
||||||
|
</Ao>
|
||||||
|
</Point>
|
||||||
|
</Root>
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
Modbus 从站 (Slave) 配置文件
|
||||||
|
对外提供 Modbus TCP/RTU 服务,将 RTU 内部信号映射为 Modbus 数据区
|
||||||
|
-->
|
||||||
|
<Root>
|
||||||
|
<!-- ============================================================
|
||||||
|
从站参数
|
||||||
|
mode: "tcp" 或 "rtu"
|
||||||
|
slave_id: 从站地址 (1~247)
|
||||||
|
host: TCP 模式监听地址 ("0.0.0.0" 表示所有接口)
|
||||||
|
port: TCP 模式监听端口 (默认 502)
|
||||||
|
device: RTU 模式串口设备
|
||||||
|
baud: RTU 模式波特率
|
||||||
|
parity: RTU 模式校验位 (N/E/O)
|
||||||
|
data_bit: RTU 模式数据位
|
||||||
|
stop_bit: RTU 模式停止位
|
||||||
|
max_clients: TCP 最大并发客户端数
|
||||||
|
============================================================ -->
|
||||||
|
<Para desc="从站参数" mode="tcp" slave_id="1" host="0.0.0.0" port="502"
|
||||||
|
device="/dev/ttyS1" baud="9600" parity="N" data_bit="8" stop_bit="1"
|
||||||
|
max_clients="4" />
|
||||||
|
|
||||||
|
<!-- ============================================================
|
||||||
|
信号映射表
|
||||||
|
============================================================
|
||||||
|
将 RTU 内部数据中心的信号映射到 Modbus 数据区。
|
||||||
|
|
||||||
|
no — 信号序号
|
||||||
|
type — 数据类型: 2=bit(开关量), 4=uint16, 6=float32,
|
||||||
|
7=uint32, 13=string
|
||||||
|
addr — 寄存器/线圈地址 (0-based)
|
||||||
|
link — 关联的数据中心信号地址
|
||||||
|
desc — 中文描述
|
||||||
|
|
||||||
|
Modbus 数据区说明:
|
||||||
|
St — 离散输入 (1X) / 线圈 (0X) — 只读/读写 bit
|
||||||
|
Mx — 输入寄存器 (3X) — 只读 16bit
|
||||||
|
Co — 线圈 (0X) — 可写 bit (外部主站控制)
|
||||||
|
Ao — 保持寄存器 (4X) — 可读写 16bit
|
||||||
|
Param — 保持寄存器 (4X) 定值区 — 可读写 16bit
|
||||||
|
============================================================ -->
|
||||||
|
|
||||||
|
<!-- ========== 遥信 (St) — 离散输入区 (1X) ========== -->
|
||||||
|
<St desc="遥信">
|
||||||
|
<Signal no="0" type="2" addr="0" link="self_ptl.st.1" desc="遥信01-开关分位" />
|
||||||
|
<Signal no="1" type="2" addr="1" link="self_ptl.st.2" desc="遥信02-开关合位" />
|
||||||
|
<Signal no="2" type="2" addr="2" link="self_ptl.st.3" desc="遥信03-未储能" />
|
||||||
|
<Signal no="3" type="2" addr="3" link="self_ptl.st.4" desc="遥信04-低气压闭锁" />
|
||||||
|
<Signal no="4" type="2" addr="4" link="self_ptl.st.5" desc="遥信05-远方/就地" />
|
||||||
|
<Signal no="5" type="2" addr="5" link="self_ptl.st.6" desc="遥信06-保护动作" />
|
||||||
|
<Signal no="6" type="2" addr="6" link="self_ptl.st.7" desc="遥信07-重合闸动作" />
|
||||||
|
<Signal no="7" type="2" addr="7" link="self_ptl.st.8" desc="遥信08-装置故障" />
|
||||||
|
<Signal no="8" type="2" addr="8" link="self_ptl.st.9" desc="遥信09-联络开关" />
|
||||||
|
<Signal no="9" type="2" addr="9" link="self_ptl.st.10" desc="遥信10-解锁" />
|
||||||
|
<Signal no="10" type="2" addr="10" link="self_ptl.st.36" desc="遥信11-硬遥信:采集硬遥信1" />
|
||||||
|
<Signal no="11" type="2" addr="11" link="self_ptl.st.37" desc="遥信12-硬遥信:采集硬遥信2" />
|
||||||
|
<Signal no="12" type="2" addr="12" link="self_ptl.st.38" desc="遥信13-硬遥信:采集硬遥信3" />
|
||||||
|
<Signal no="13" type="2" addr="13" link="self_ptl.st.39" desc="遥信14-硬遥信:采集硬遥信4" />
|
||||||
|
<Signal no="14" type="2" addr="14" link="self_ptl.st.40" desc="遥信15-硬遥信:采集硬遥信5" />
|
||||||
|
<Signal no="15" type="2" addr="15" link="self_ptl.st.41" desc="遥信16-硬遥信:采集硬遥信6" />
|
||||||
|
|
||||||
|
<!-- 自检遥信区 (addr 32~63) -->
|
||||||
|
<Signal no="16" type="2" addr="32" link="self_ptl.st.70" desc="自检-文件系统挂载异常" />
|
||||||
|
<Signal no="17" type="2" addr="33" link="self_ptl.st.95" desc="自检-装置闭锁" />
|
||||||
|
<Signal no="18" type="2" addr="34" link="self_ptl.st.108" desc="自检-终端故障" />
|
||||||
|
<Signal no="19" type="2" addr="35" link="self_ptl.st.109" desc="自检-参数装载异常" />
|
||||||
|
<Signal no="20" type="2" addr="36" link="self_ptl.st.110" desc="自检-装置检修" />
|
||||||
|
<Signal no="21" type="2" addr="37" link="self_ptl.st.78" desc="自检-主站通讯异常" />
|
||||||
|
<Signal no="22" type="2" addr="38" link="self_ptl.st.132" desc="自检-故障指示" />
|
||||||
|
<Signal no="23" type="2" addr="39" link="self_ptl.st.143" desc="自检-开关位置状态" />
|
||||||
|
<Signal no="24" type="2" addr="40" link="self_ptl.st.144" desc="自检-远方投入" />
|
||||||
|
</St>
|
||||||
|
|
||||||
|
<!-- ========== 遥测 (Mx) — 输入寄存器区 (3X) ========== -->
|
||||||
|
<Mx desc="遥测">
|
||||||
|
<!-- 浮点数占用 2 个连续寄存器, addr 递增 2 -->
|
||||||
|
<Signal no="0" type="6" addr="0" link="self_ptl.mx.1" desc="遥测01-经度" />
|
||||||
|
<Signal no="1" type="6" addr="2" link="self_ptl.mx.5" desc="遥测02-纬度" />
|
||||||
|
<Signal no="2" type="6" addr="4" link="self_ptl.mx.9" desc="遥测03-海拔" />
|
||||||
|
<Signal no="3" type="6" addr="6" link="self_ptl.mx.38" desc="遥测04-频率" />
|
||||||
|
<Signal no="4" type="6" addr="8" link="self_ptl.mx.64" desc="遥测05-采集板温度" />
|
||||||
|
|
||||||
|
<!-- 电气量 -->
|
||||||
|
<Signal no="5" type="6" addr="10" link="iec61850m.prj.mx.0" desc="遥测06-A相电压" />
|
||||||
|
<Signal no="6" type="6" addr="12" link="iec61850m.prj.mx.1" desc="遥测07-B相电压" />
|
||||||
|
<Signal no="7" type="6" addr="14" link="iec61850m.prj.mx.2" desc="遥测08-C相电压" />
|
||||||
|
<Signal no="8" type="6" addr="16" link="iec61850m.prj.mx.3" desc="遥测09-A相电流" />
|
||||||
|
<Signal no="9" type="6" addr="18" link="iec61850m.prj.mx.4" desc="遥测10-有功功率" />
|
||||||
|
<Signal no="10" type="6" addr="20" link="iec61850m.prj.mx.5" desc="遥测11-无功功率" />
|
||||||
|
<Signal no="11" type="6" addr="22" link="iec61850m.prj.mx.6" desc="遥测12-功率因数" />
|
||||||
|
</Mx>
|
||||||
|
|
||||||
|
<!-- ========== 遥控 (Co) — 线圈区 (0X), 可写 ========== -->
|
||||||
|
<Co desc="遥控">
|
||||||
|
<Signal no="0" type="2" addr="0" link="self_ptl.co.11" desc="遥控01-交流录波" />
|
||||||
|
<Signal no="1" type="2" addr="1" link="iec61850m.prj.co.0" desc="遥控02-分闸" />
|
||||||
|
<Signal no="2" type="2" addr="2" link="iec61850m.prj.co.1" desc="遥控03-合闸" />
|
||||||
|
<Signal no="3" type="2" addr="3" link="iec61850m.prj.co.2" desc="遥控04-复归" />
|
||||||
|
<Signal no="4" type="2" addr="4" link="iec61850m.prj.co.3" desc="遥控05-保护投退" />
|
||||||
|
<Signal no="5" type="2" addr="5" link="iec61850m.prj.co.4" desc="遥控06-低频保护软压板" />
|
||||||
|
</Co>
|
||||||
|
|
||||||
|
<!-- ========== 参数 (Ao) — 保持寄存器区 (4X), 可读写 ========== -->
|
||||||
|
<Ao desc="参数">
|
||||||
|
<!-- 字符串类型 -->
|
||||||
|
<Signal no="0" type="13" addr="0" link="self_ptl.ao.INFO_DEV_TYPE" desc="参数01-终端类型" />
|
||||||
|
<Signal no="1" type="13" addr="8" link="self_ptl.ao.TERM_ID" desc="参数02-终端ID号" />
|
||||||
|
<!-- float 类型 -->
|
||||||
|
<Signal no="2" type="6" addr="16" link="self_ptl.ao.I_DEAD" desc="参数03-交流电流死区" />
|
||||||
|
<Signal no="3" type="6" addr="18" link="self_ptl.ao.UAC_DEAD" desc="参数04-交流电压死区" />
|
||||||
|
<!-- uint32 类型 -->
|
||||||
|
<Signal no="4" type="7" addr="20" link="self_ptl.ao.FaultLed_Reset_T" desc="参数05-故障灯复归时间" />
|
||||||
|
<!-- uint16 类型 -->
|
||||||
|
<Signal no="5" type="4" addr="22" link="self_ptl.ao.ALARMLED_EN_MOD" desc="参数06-故障指示灯自动复归投退" />
|
||||||
|
</Ao>
|
||||||
|
|
||||||
|
<!-- ========== 定值 (Param) — 保持寄存器区 (4X), 可读写 ========== -->
|
||||||
|
<Param desc="定值">
|
||||||
|
<Signal no="0" type="6" addr="0" link="iec61850m.prj.param.0" desc="定值01-过流I段定值" />
|
||||||
|
<Signal no="1" type="6" addr="2" link="iec61850m.prj.param.1" desc="定值02-过流II段定值" />
|
||||||
|
</Param>
|
||||||
|
</Root>
|
||||||
|
|
@ -4,10 +4,11 @@
|
||||||
{"name": "app_sys", "enable": true, "comment": "系统应用(数据中心,必须启用)"},
|
{"name": "app_sys", "enable": true, "comment": "系统应用(数据中心,必须启用)"},
|
||||||
{"name": "app_cmd", "enable": true, "comment": "命令行应用(SSH终端交互)"},
|
{"name": "app_cmd", "enable": true, "comment": "命令行应用(SSH终端交互)"},
|
||||||
{"name": "app_web_server", "enable": true, "comment": "Web 服务器(HTTP/WS,端口8000)"},
|
{"name": "app_web_server", "enable": true, "comment": "Web 服务器(HTTP/WS,端口8000)"},
|
||||||
{"name": "app_com_decode", "enable": true, "comment": "通讯编解码应用(串口/TCP收发)"},
|
{"name": "app_com_decode", "enable": false, "comment": "通讯编解码应用(串口/TCP收发)"},
|
||||||
{"name": "app_iec", "enable": true, "comment": "IEC 101/104 协议应用"},
|
{"name": "app_iec", "enable": false, "comment": "IEC 101/104 协议应用"},
|
||||||
{"name": "app_self_ptl", "enable": true, "comment": "ICP67 私有规约应用"},
|
{"name": "app_self_ptl", "enable": false, "comment": "ICP67 私有规约应用"},
|
||||||
{"name": "app_iec61850m", "enable": true, "comment": "IEC 61850 MMS 客户端"},
|
{"name": "app_iec61850m", "enable": false, "comment": "IEC 61850 MMS 客户端"},
|
||||||
{"name": "app_iec61850s", "enable": true, "comment": "IEC 61850 MMS 服务端"}
|
{"name": "app_iec61850s", "enable": false, "comment": "IEC 61850 MMS 服务端"},
|
||||||
|
{"name": "app_modbus_m", "enable": true, "comment": "Modbus 主站(TCP/RTU 多通道)"}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -245,6 +245,26 @@ body.login-page #main-content { margin-left: 0; display: flex; align-items: cent
|
||||||
.dash-card.monitor { border-top: 4px solid #607D8B; }
|
.dash-card.monitor { border-top: 4px solid #607D8B; }
|
||||||
.dash-card.terminal { border-top: 4px solid #00BCD4; }
|
.dash-card.terminal { border-top: 4px solid #00BCD4; }
|
||||||
|
|
||||||
|
/* === 批量操作工具栏 === */
|
||||||
|
.batch-toolbar {
|
||||||
|
background: var(--card-bg);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
padding: 24px 28px;
|
||||||
|
margin-top: 28px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.batch-toolbar h3 {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
color: var(--nav-bg);
|
||||||
|
}
|
||||||
|
.batch-toolbar p {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
/* === Page Header === */
|
/* === Page Header === */
|
||||||
.page-header {
|
.page-header {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
|
|
||||||
|
|
@ -263,7 +263,10 @@ var App = (function() {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
init: init,
|
init: init,
|
||||||
dispatch: dispatch
|
dispatch: dispatch,
|
||||||
|
getSignalData: function(type) {
|
||||||
|
return lastDataCache[type];
|
||||||
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,170 @@ function fmtVal(v) {
|
||||||
return String(v).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
return String(v).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------
|
||||||
|
* XML 工具函数(导入导出)
|
||||||
|
* ------------------------------------------------------------------------ */
|
||||||
|
function escapeXml(str) {
|
||||||
|
if (typeof str !== 'string') return String(str);
|
||||||
|
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出所有5种信号类型(out/in/yk/ao/param)到一个XML文件
|
||||||
|
*/
|
||||||
|
function exportAllToXml() {
|
||||||
|
var types = ['out', 'in', 'yk', 'ao', 'param'];
|
||||||
|
var typeLabels = { out:'注册', in:'链接注册', yk:'遥控', ao:'参数', param:'定值' };
|
||||||
|
var hasData = false;
|
||||||
|
|
||||||
|
var xml = '<?xml version="1.0" encoding="UTF-8"?>\n<root>\n';
|
||||||
|
|
||||||
|
for (var t = 0; t < types.length; t++) {
|
||||||
|
var st = types[t];
|
||||||
|
var data = null;
|
||||||
|
if (typeof App !== 'undefined' && App.getSignalData) {
|
||||||
|
data = App.getSignalData(st);
|
||||||
|
}
|
||||||
|
if (!data || !Array.isArray(data) || data.length === 0) {
|
||||||
|
xml += ' <signals type="' + st + '"/>\n';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
hasData = true;
|
||||||
|
xml += ' <signals type="' + st + '">\n';
|
||||||
|
for (var i = 0; i < data.length; i++) {
|
||||||
|
xml += ' <signal no="' + i + '" saddr="' + escapeXml(data[i].saddr) + '"/>\n';
|
||||||
|
}
|
||||||
|
xml += ' </signals>\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
xml += '</root>\n';
|
||||||
|
|
||||||
|
if (!hasData) {
|
||||||
|
alert('当前没有任何信号数据,无法导出');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var blob = new Blob([xml], { type: 'application/xml;charset=utf-8' });
|
||||||
|
var url = URL.createObjectURL(blob);
|
||||||
|
var a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'signals_all_' + Date.now() + '.xml';
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 XML 文件导入所有5种信号类型,按 type 分组批量执行 add
|
||||||
|
*/
|
||||||
|
function importAllFromXml() {
|
||||||
|
var input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.accept = '.xml';
|
||||||
|
input.style.display = 'none';
|
||||||
|
document.body.appendChild(input);
|
||||||
|
|
||||||
|
input.onchange = function(e) {
|
||||||
|
var file = e.target.files[0];
|
||||||
|
if (!file) {
|
||||||
|
document.body.removeChild(input);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var reader = new FileReader();
|
||||||
|
reader.onload = function(ev) {
|
||||||
|
document.body.removeChild(input);
|
||||||
|
try {
|
||||||
|
var parser = new DOMParser();
|
||||||
|
var xmlDoc = parser.parseFromString(ev.target.result, 'text/xml');
|
||||||
|
|
||||||
|
var parseError = xmlDoc.querySelector('parsererror');
|
||||||
|
if (parseError) {
|
||||||
|
alert('XML 解析失败:' + parseError.textContent);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var typeNodes = xmlDoc.querySelectorAll('signals');
|
||||||
|
if (typeNodes.length === 0) {
|
||||||
|
alert('XML 文件中没有 <signals> 元素');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var validTypes = { out: '注册', in: '链接注册', yk: '遥控', ao: '参数', param: '定值' };
|
||||||
|
var totalCount = 0;
|
||||||
|
var typeCounts = {};
|
||||||
|
|
||||||
|
for (var ti = 0; ti < typeNodes.length; ti++) {
|
||||||
|
var signalsEl = typeNodes[ti];
|
||||||
|
var fileType = signalsEl.getAttribute('type');
|
||||||
|
if (!fileType || !validTypes[fileType]) continue;
|
||||||
|
|
||||||
|
var signals = [];
|
||||||
|
var children = signalsEl.children;
|
||||||
|
for (var si = 0; si < children.length; si++) {
|
||||||
|
if (children[si].tagName !== 'signal') continue;
|
||||||
|
var saddr = children[si].getAttribute('saddr');
|
||||||
|
if (!saddr || !saddr.trim()) continue;
|
||||||
|
var noAttr = children[si].getAttribute('no');
|
||||||
|
signals.push({
|
||||||
|
no: noAttr !== null ? parseInt(noAttr) : si,
|
||||||
|
saddr: saddr.trim()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signals.length > 0) {
|
||||||
|
typeCounts[fileType] = signals.length;
|
||||||
|
totalCount += signals.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalCount === 0) {
|
||||||
|
alert('XML 文件中没有有效的信号数据');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var msg = '即将导入以下信号:\n';
|
||||||
|
for (var t in typeCounts) {
|
||||||
|
msg += ' ' + validTypes[t] + ' (' + t + '): ' + typeCounts[t] + ' 个\n';
|
||||||
|
}
|
||||||
|
msg += '\n共 ' + totalCount + ' 个信号,是否继续?';
|
||||||
|
|
||||||
|
if (!confirm(msg)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var ti2 = 0; ti2 < typeNodes.length; ti2++) {
|
||||||
|
var signalsEl2 = typeNodes[ti2];
|
||||||
|
var fileType2 = signalsEl2.getAttribute('type');
|
||||||
|
if (!fileType2 || !validTypes[fileType2]) continue;
|
||||||
|
|
||||||
|
var children2 = signalsEl2.children;
|
||||||
|
for (var si2 = 0; si2 < children2.length; si2++) {
|
||||||
|
if (children2[si2].tagName !== 'signal') continue;
|
||||||
|
var saddr2 = children2[si2].getAttribute('saddr');
|
||||||
|
if (!saddr2 || !saddr2.trim()) continue;
|
||||||
|
WsClient.send({
|
||||||
|
saddr: saddr2.trim(),
|
||||||
|
signal_type: fileType2,
|
||||||
|
curd: 'add',
|
||||||
|
setting_zone: '0',
|
||||||
|
signal_data: ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
alert('导入完成,共发送 ' + totalCount + ' 个 add 请求');
|
||||||
|
} catch (ex) {
|
||||||
|
alert('导入失败:' + ex.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
input.click();
|
||||||
|
}
|
||||||
|
|
||||||
/* ========================================================================
|
/* ========================================================================
|
||||||
* 共用工具栏 HTML
|
* 共用工具栏 HTML
|
||||||
* ======================================================================== */
|
* ======================================================================== */
|
||||||
|
|
@ -60,7 +224,7 @@ function makeToolbar(signalType, showSet) {
|
||||||
+ '</div>';
|
+ '</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
function bindToolbar(signalType) {
|
function bindToolbar(signalType, getDataFn) {
|
||||||
var execBtn = document.getElementById('tb-exec');
|
var execBtn = document.getElementById('tb-exec');
|
||||||
var clearBtn = document.getElementById('tb-clear');
|
var clearBtn = document.getElementById('tb-clear');
|
||||||
var saddrInput = document.getElementById('tb-saddr');
|
var saddrInput = document.getElementById('tb-saddr');
|
||||||
|
|
@ -140,7 +304,16 @@ var DashboardPage = {
|
||||||
+ '</a>';
|
+ '</a>';
|
||||||
}
|
}
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
|
html += '<div class="batch-toolbar">'
|
||||||
|
+ '<h3>📦 批量操作</h3>'
|
||||||
|
+ '<p>导入/导出所有信号类型 (out/in/yk/ao/param) 到一个 XML 文件</p>'
|
||||||
|
+ '<button class="btn btn-info" id="btn-export-all" style="margin-right:12px">📤 批量导出所有信号</button>'
|
||||||
|
+ '<button class="btn btn-warn" id="btn-import-all">📥 批量导入所有信号</button>'
|
||||||
|
+ '</div>';
|
||||||
container.innerHTML = html;
|
container.innerHTML = html;
|
||||||
|
|
||||||
|
document.getElementById('btn-export-all').onclick = exportAllToXml;
|
||||||
|
document.getElementById('btn-import-all').onclick = importAllFromXml;
|
||||||
},
|
},
|
||||||
onEnter: function() {},
|
onEnter: function() {},
|
||||||
onLeave: function() {}
|
onLeave: function() {}
|
||||||
|
|
@ -188,7 +361,7 @@ var OutPage = (function() {
|
||||||
+ '<div class="page-header"><h2>注册管理</h2><p>out — 状态量信号,注册后支持添加、删除、修改值</p></div>'
|
+ '<div class="page-header"><h2>注册管理</h2><p>out — 状态量信号,注册后支持添加、删除、修改值</p></div>'
|
||||||
+ makeToolbar('out')
|
+ makeToolbar('out')
|
||||||
+ renderTable('out-tbody', headers, renderTableHtml(), '暂无注册数据,请先添加信号');
|
+ renderTable('out-tbody', headers, renderTableHtml(), '暂无注册数据,请先添加信号');
|
||||||
bindToolbar('out');
|
bindToolbar('out', function() { return data; });
|
||||||
bindModifyButtons();
|
bindModifyButtons();
|
||||||
},
|
},
|
||||||
onEnter: function() {},
|
onEnter: function() {},
|
||||||
|
|
@ -233,7 +406,7 @@ var InPage = (function() {
|
||||||
+ '<div class="page-header"><h2>链接注册管理</h2><p>in — 测量量信号,链接注册后仅支持添加和删除,不支持修改</p></div>'
|
+ '<div class="page-header"><h2>链接注册管理</h2><p>in — 测量量信号,链接注册后仅支持添加和删除,不支持修改</p></div>'
|
||||||
+ makeToolbar('in')
|
+ makeToolbar('in')
|
||||||
+ renderTable('in-tbody', headers, renderTableHtml(), '暂无链接注册数据,请先添加信号');
|
+ renderTable('in-tbody', headers, renderTableHtml(), '暂无链接注册数据,请先添加信号');
|
||||||
bindToolbar('in');
|
bindToolbar('in', function() { return data; });
|
||||||
},
|
},
|
||||||
onEnter: function() {},
|
onEnter: function() {},
|
||||||
onLeave: function() { data = []; },
|
onLeave: function() { data = []; },
|
||||||
|
|
@ -298,7 +471,7 @@ var YkPage = (function() {
|
||||||
+ '<div class="page-header"><h2>遥控管理</h2><p>yk — 遥控分合闸控制,控合=1(闭合),控分=0(断开)</p></div>'
|
+ '<div class="page-header"><h2>遥控管理</h2><p>yk — 遥控分合闸控制,控合=1(闭合),控分=0(断开)</p></div>'
|
||||||
+ makeToolbar('yk')
|
+ makeToolbar('yk')
|
||||||
+ renderTable('yk-tbody', headers, renderTableHtml(), '暂无遥控数据,请先添加信号');
|
+ renderTable('yk-tbody', headers, renderTableHtml(), '暂无遥控数据,请先添加信号');
|
||||||
bindToolbar('yk');
|
bindToolbar('yk', function() { return data; });
|
||||||
bindButtons();
|
bindButtons();
|
||||||
},
|
},
|
||||||
onEnter: function() {},
|
onEnter: function() {},
|
||||||
|
|
@ -371,7 +544,7 @@ var AoPage = (function() {
|
||||||
+ '<div class="page-header"><h2>参数管理</h2><p>ao — 模拟输出参数设定,支持单值修改</p></div>'
|
+ '<div class="page-header"><h2>参数管理</h2><p>ao — 模拟输出参数设定,支持单值修改</p></div>'
|
||||||
+ makeToolbar('ao')
|
+ makeToolbar('ao')
|
||||||
+ renderTable('ao-tbody', headers, renderTableHtml(), '暂无参数数据,请先添加信号');
|
+ renderTable('ao-tbody', headers, renderTableHtml(), '暂无参数数据,请先添加信号');
|
||||||
bindToolbar('ao');
|
bindToolbar('ao', function() { return data; });
|
||||||
bindModifyButtons();
|
bindModifyButtons();
|
||||||
},
|
},
|
||||||
onEnter: function() {},
|
onEnter: function() {},
|
||||||
|
|
@ -496,7 +669,7 @@ var ParamPage = (function() {
|
||||||
+ '<div class="page-header"><h2>定值管理</h2><p>param — 多定值区参数管理,点击行展开查看各定值区详情并可修改</p></div>'
|
+ '<div class="page-header"><h2>定值管理</h2><p>param — 多定值区参数管理,点击行展开查看各定值区详情并可修改</p></div>'
|
||||||
+ makeToolbar('param')
|
+ makeToolbar('param')
|
||||||
+ renderTable('param-tbody', headers, renderTableHtml(), '暂无定值数据,请先添加信号');
|
+ renderTable('param-tbody', headers, renderTableHtml(), '暂无定值数据,请先添加信号');
|
||||||
bindToolbar('param');
|
bindToolbar('param', function() { return data; });
|
||||||
bindEvents();
|
bindEvents();
|
||||||
},
|
},
|
||||||
onEnter: function() {},
|
onEnter: function() {},
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue