<修改> 1、消除cmd中的编译告警;2、消除log中的编译告警;3、增加modbus客户端程序;4、webserver增加导入导出信号表功能,可以导出已增加的信号,也可以一键导入想要观察的信号

This commit is contained in:
ypc 2026-06-24 15:43:41 +08:00
parent c6501da92f
commit 19a2f2a3c0
18 changed files with 5024 additions and 2840 deletions

View File

@ -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 命令逻辑正确 | ✅(代码审查确认) |
| 编译通过 | ✅ |

View File

@ -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 )

View File

@ -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

View File

@ -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 $<

View File

@ -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

View File

@ -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));

View File

@ -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()

View File

@ -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);
// 检查文件大小 // 检查文件大小

View File

@ -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;
} }

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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 多通道)"}
] ]
} }

View File

@ -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;

View File

@ -263,7 +263,10 @@ var App = (function() {
return { return {
init: init, init: init,
dispatch: dispatch dispatch: dispatch,
getSignalData: function(type) {
return lastDataCache[type];
}
}; };
})(); })();

View File

@ -37,6 +37,170 @@ function fmtVal(v) {
return String(v).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'); return String(v).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
} }
/* ------------------------------------------------------------------------
* XML 工具函数导入导出
* ------------------------------------------------------------------------ */
function escapeXml(str) {
if (typeof str !== 'string') return String(str);
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&apos;');
}
/**
* 导出所有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() {},