feat(libweb_server): Web 管理服务模块(3 文件拆分 + 单线程 + EV_STOP)

基于原工程 libweb_server 重构,核心改进:
- 双线程 → 单线程: mg_mgr_poll 合入 X-Macro 框架 EV_TIMER1
- 无停止机制 → EV_STOP 优雅退出
- 1500 行 → ~650 行(3 文件拆分: web_server.cpp/ws_method.cpp)
- 端口: http://0.0.0.0:8080, WebSocket: /ws
- 信号 CRUD: add/del/set × 5 种信号类型
- 增量推送: EV_TIMER2 每 100ms 对比 last_val
- 异步命令通道 + 文件上传/下载 + 远程命令执行

注册: app_modules.h + system/makefile + copy_headers.sh + RTU/makefile
构建: x86 + ARM 全通过,零警告
This commit is contained in:
ypc 2026-07-08 17:57:17 +08:00
parent db1ee1e979
commit 3dd52d2202
9 changed files with 1213 additions and 5 deletions

View File

@ -40,7 +40,7 @@
| IEC 点表解析 | `libiec` | ⬜ |
| Modbus 主站管理 | `libmodbus_m`(原工程自研模块,导入+适配) | ✅ |
| 通讯通道解码 | `libcom_decode` | ⬜ |
| Web 管理服务 | `libweb_server` | ⬜ |
| Web 管理服务 | `libweb_server`3 文件拆分单线程EV_STOP 支持) | ✅ |
| Mongoose 网络库 | `libmongoose`(开源库 v7.22,导入+适配) | ✅ |
| 主程序入口 | `RTU`X-Macro 框架 + app_config.json 启停控制 + 独立 config/ 目录) | ✅ |

View File

@ -108,5 +108,6 @@ copy_proto_module "libmongoose" "mongoose.h"
# 系统模块system/
# ================================================================
copy_sys_header "mySystem.h"
copy_sys_module "libweb_server" "web_server.h"
echo "=== done ==="

View File

@ -4,6 +4,7 @@ ROOT := $(realpath $(CURDIR)/../../../..)
INC_PATH := -I$(ROOT)/release/inc \
-I$(ROOT)/src/system/inc \
-I$(ROOT)/src/system/libweb_server/inc \
-I$(ROOT)/src/public/libdatacenter/inc \
-I$(ROOT)/src/public/liblog/inc \
-I$(ROOT)/src/public/libfunc/inc \
@ -11,11 +12,12 @@ INC_PATH := -I$(ROOT)/release/inc \
-I$(ROOT)/src/public/libtask/inc \
-I$(ROOT)/src/public/libmy_xxhash/inc \
-I$(ROOT)/src/public/libcJSON/inc \
-I$(ROOT)/src/protocol/libmodbus/inc
-I$(ROOT)/src/protocol/libmodbus/inc \
-I$(ROOT)/src/protocol/libmongoose/inc
LIBS := -L$(ROOT)/release/$(BUILD_TYPE)/lib \
-lmodbus_m -lmodbus -l60870 \
-ldatacenter -lxml -lcmd -ltask \
-lweb_server -lmodbus_m -lmodbus -l60870 \
-lmongoose -ldatacenter -lxml -lcmd -ltask \
-llog -lfunc -lmy_xxhash -lcJSON -lmd5 -lcomm \
-lpthread -lstdc++

View File

@ -0,0 +1,39 @@
include ./../../../linux.mk
L := $(notdir $(realpath $(CURDIR)/..))
M := libweb_server
O := $(LIB_REL)/$(M).a
S := $(SRC_ROOT_DIR)/$(L)/libweb_server/src
I := -I$(SRC_ROOT_DIR)/$(L)/libweb_server/inc \
-I$(SRC_ROOT_DIR)/$(L)/inc \
-I$(SRC_ROOT_DIR)/protocol/libmongoose/inc \
-I$(SRC_ROOT_DIR)/public/libdatacenter/inc \
-I$(SRC_ROOT_DIR)/public/liblog/inc \
-I$(SRC_ROOT_DIR)/public/libfunc/inc \
-I$(SRC_ROOT_DIR)/public/libcmd/inc \
-I$(SRC_ROOT_DIR)/public/libtask/inc \
-I$(SRC_ROOT_DIR)/public/libcJSON/inc
B := $(CURDIR)/$(M)/obj
SRCS := $(wildcard $(S)/*.cpp)
OBJS := $(patsubst $(S)/%.cpp, $(B)/%.o, $(SRCS))
F := $(CXX_FLAGS) $(I)
.PHONY: all
all:
@mkdir -p $(ROOT_DIR)/release/inc
@$(MAKE) $(O)
$(O): $(OBJS)
@mkdir -p $(dir $@)
$(AR) rcs $@ $^
@echo "[$(M)] built"
$(B)/%.o: $(S)/%.cpp
@mkdir -p $(dir $@)
$(CXX) $(F) -c $< -o $@
.PHONY: clean
clean:
rm -rf $(B) $(O)
.PHONY: rebuild
rebuild: clean all

View File

@ -1,7 +1,7 @@
include ./../../linux.mk
# app_sys.cpp 已合并到 RTU makefile 中,此处仅管理子模块
SUBDIRS := ./libmodbus_m ./RTU
SUBDIRS := ./libmodbus_m ./libweb_server ./RTU
.PHONY: all clean rebuild

View File

@ -19,6 +19,7 @@
/* ============== 线程模块system/ 子目录) ============== */
APP_MODULE(MODBUS_M, modbus_m, app_modbus_m_init1, app_modbus_m_init2, app_modbus_m)
APP_MODULE(WEB_SERVER, web_server, app_web_server_init1, app_web_server_init2, app_web_server)
/* ============== 预留模块(阶段 2-3 实现后取消注释) ============== */
// APP_MODULE(COM_DECODE, com_decode, app_com_decode_init1, app_com_decode_init2, app_com_decode)

View File

@ -0,0 +1,59 @@
/**
* @file web_server.h
* @brief Web HTTP REST API + WebSocket
* @details mongoose v7.22 Web
* RTU X-Macro 线
*
* :
* - HTTP packed_fs
* - WebSocket
* - 5 out/in/yk/ao/param
* - stdout
* - /
* -
*/
#ifndef _WEB_SERVER_H_
#define _WEB_SERVER_H_
#include "myBase.h"
#include "mySystem.h"
#ifdef __cplusplus
extern "C"
{
#endif
#define MODULE_WEB_SERVER "web_server"
/** WebSocket 信号类型 */
typedef enum
{
WS_SIG_OUT = 0,
WS_SIG_IN,
WS_SIG_YK,
WS_SIG_AO,
WS_SIG_PARAM,
WS_SIG_MAX
} enum_ws_sig_type;
struct mg_connection;
/* ---- X-Macro 框架入口 ---- */
int app_web_server_init1(void *arg);
int app_web_server_init2(void *arg);
void *app_web_server(void *arg);
/* ---- 广播/单播发送 ---- */
void ws_send_all(const char *p_tx, size_t tx_len);
bool ws_send_one(unsigned long conn_id, const char *p_tx, size_t tx_len);
bool ws_send_binary(unsigned long conn_id, const void *data, size_t len);
/* ---- 异步命令通道 ---- */
void ws_set_async_cmd_conn(const char *module, unsigned long conn_id);
unsigned long ws_get_async_cmd_conn(const char *module);
#ifdef __cplusplus
}
#endif
#endif /* _WEB_SERVER_H_ */

View File

@ -0,0 +1,435 @@
/**
* @file web_server.cpp
* @brief HTTP/WebSocket Mongoose +
* @details app_web_server 线 EV_TIMER1 mg_mgr_poll
* 线 X-Macro 线
* EV_TIMER2: ws_push_task()
* EV_TIMER3: run_cnt++
* EV_STOP: 退
*/
#include "web_server.h"
#include "myLog.h"
#include "myMongoose.h"
#include "cJSON.h"
#include "myFunc.h"
#include <string>
#include <vector>
#include <map>
#include <pthread.h>
#include <csignal>
/* ---- 硬编码配置 ---- */
#define WS_LISTEN_ON "http://0.0.0.0:8080"
#define WS_UPGRADE_URI "/ws"
/* ---- 全局状态 ---- */
static struct mg_mgr s_ws_mgr;
static std::vector<mg_connection *> g_ws_conns;
static pthread_mutex_t g_ws_conns_mutex = PTHREAD_MUTEX_INITIALIZER;
/* ---- 前向声明 ---- */
void ws_recv(struct mg_connection *c, const char *p_rx, uint16_t rx_len);
void ws_session_destroy(struct mg_connection *c);
void ws_push_task(void);
/* ---- 广播发送 ---- */
void ws_send_all(const char *p_tx, size_t tx_len)
{
if (!p_tx || tx_len == 0)
{
return;
}
pthread_mutex_lock(&g_ws_conns_mutex);
for (size_t i = 0; i < g_ws_conns.size(); i++)
{
mg_connection *c = g_ws_conns[i];
if (c->is_websocket && !c->is_draining)
{
mg_ws_send(c, p_tx, tx_len, WEBSOCKET_OP_TEXT);
}
}
pthread_mutex_unlock(&g_ws_conns_mutex);
}
/* ---- 单连接发送 ---- */
bool ws_send_one(unsigned long conn_id, const char *p_tx, size_t tx_len)
{
if (!p_tx || tx_len == 0)
{
return false;
}
bool ok = false;
pthread_mutex_lock(&g_ws_conns_mutex);
for (size_t i = 0; i < g_ws_conns.size(); i++)
{
mg_connection *c = g_ws_conns[i];
if (c->id == conn_id && c->is_websocket && !c->is_draining)
{
size_t sent = mg_ws_send(c, p_tx, tx_len, WEBSOCKET_OP_TEXT);
ok = (sent >= tx_len);
break;
}
}
pthread_mutex_unlock(&g_ws_conns_mutex);
return ok;
}
/* ---- 二进制发送 ---- */
bool ws_send_binary(unsigned long conn_id, const void *data, size_t len)
{
if (!data || len == 0)
{
return false;
}
bool ok = false;
pthread_mutex_lock(&g_ws_conns_mutex);
for (size_t i = 0; i < g_ws_conns.size(); i++)
{
mg_connection *c = g_ws_conns[i];
if (c->id == conn_id && c->is_websocket && !c->is_draining)
{
size_t sent = mg_ws_send(c, data, len, WEBSOCKET_OP_BINARY);
ok = (sent >= len);
break;
}
}
pthread_mutex_unlock(&g_ws_conns_mutex);
return ok;
}
/* ---- 连接列表管理 ---- */
static void ws_conn_remove(mg_connection *c)
{
pthread_mutex_lock(&g_ws_conns_mutex);
for (size_t i = 0; i < g_ws_conns.size(); i++)
{
if (g_ws_conns[i] == c)
{
g_ws_conns.erase(g_ws_conns.begin() + i);
break;
}
}
pthread_mutex_unlock(&g_ws_conns_mutex);
}
/* ---- Mongoose 事件处理器 ---- */
static void ws_event_handler(mg_connection *c, int ev, void *ev_data)
{
if (ev == MG_EV_HTTP_MSG)
{
mg_http_message *hm = (mg_http_message *)ev_data;
if (mg_match(hm->uri, mg_str(WS_UPGRADE_URI), NULL))
{
mg_ws_upgrade(c, hm, NULL);
c->data[0] = 'W';
}
else
{
/* HTTP 静态文件: 默认返回 404 */
mg_http_reply(c, 404, "Content-Type: text/plain\r\n", "Not Found\n");
}
}
else if (ev == MG_EV_WS_MSG)
{
mg_ws_message *wm = (mg_ws_message *)ev_data;
bool found = false;
pthread_mutex_lock(&g_ws_conns_mutex);
for (size_t i = 0; i < g_ws_conns.size(); i++)
{
if (g_ws_conns[i] == c)
{
found = true;
break;
}
}
if (!found)
{
g_ws_conns.push_back(c);
}
pthread_mutex_unlock(&g_ws_conns_mutex);
ws_recv(c, wm->data.buf, (uint16_t)wm->data.len);
}
else if (ev == MG_EV_WS_CTL)
{
mg_ws_message *wm = (mg_ws_message *)ev_data;
uint8_t op = wm->flags & 15;
if (op == WEBSOCKET_OP_CLOSE)
{
ws_session_destroy(c);
ws_conn_remove(c);
}
}
else if (ev == MG_EV_CLOSE)
{
if (c->is_websocket)
{
ws_session_destroy(c);
ws_conn_remove(c);
}
}
}
/* ---- 命令行执行(捕获 stdout → 字符串) ---- */
#include "myCmd.h"
#include <unistd.h>
#include <cstring>
std::string ws_cmd_exec(const char *cmd_line)
{
if (!cmd_line)
{
return "";
}
char line[512];
strncpy(line, cmd_line, sizeof(line) - 1);
line[sizeof(line) - 1] = '\0';
/* strtok 解析参数 */
char *argv[16];
int argc = 0;
char *p = strtok(line, " \t");
while (p && argc < 16)
{
argv[argc++] = p;
p = strtok(NULL, " \t");
}
if (argc == 0)
{
return "";
}
stru_cmd *p_cmd = cmd_find(argv[0]);
if (!p_cmd)
{
return std::string("[unknown] ") + argv[0];
}
/* pipe 重定向 stdout */
fflush(stdout);
int pipefd[2];
if (pipe(pipefd) != 0)
{
return "pipe failed";
}
std::string result;
auto *ctx = new std::pair<int, std::string *>(pipefd[0], &result);
pthread_t reader;
pthread_create(&reader, NULL,
[](void *arg) -> void *
{
auto *cx = (std::pair<int, std::string *> *)arg;
char buf[4096];
ssize_t n;
while ((n = read(cx->first, buf, sizeof(buf))) > 0)
{
cx->second->append(buf, n);
}
close(cx->first);
delete cx;
return NULL;
},
ctx);
int saved = dup(STDOUT_FILENO);
dup2(pipefd[1], STDOUT_FILENO);
close(pipefd[1]);
p_cmd->func(argc, argv);
fflush(stdout);
dup2(saved, STDOUT_FILENO);
close(saved);
pthread_join(reader, NULL);
if (!result.empty())
{
return result;
}
return "(no output)";
}
/* ---- 异步命令通道实现 ---- */
static std::map<std::string, unsigned long> g_async_cmd_conn_map;
static pthread_mutex_t g_async_cmd_conn_mutex = PTHREAD_MUTEX_INITIALIZER;
void ws_set_async_cmd_conn(const char *module, unsigned long conn_id)
{
if (!module)
{
return;
}
pthread_mutex_lock(&g_async_cmd_conn_mutex);
g_async_cmd_conn_map[module] = conn_id;
pthread_mutex_unlock(&g_async_cmd_conn_mutex);
}
unsigned long ws_get_async_cmd_conn(const char *module)
{
if (!module)
{
return 0;
}
pthread_mutex_lock(&g_async_cmd_conn_mutex);
auto it = g_async_cmd_conn_map.find(module);
unsigned long id = (it != g_async_cmd_conn_map.end()) ? it->second : 0;
pthread_mutex_unlock(&g_async_cmd_conn_mutex);
return id;
}
/* ---- 异步命令输出(给其他模块调用) ---- */
void async_cmd_printf(const char *module, const char *fmt, ...)
{
if (!module || !fmt)
{
return;
}
unsigned long conn_id = ws_get_async_cmd_conn(module);
if (conn_id == 0)
{
return;
}
va_list ap;
char buf[2048];
va_start(ap, fmt);
int n = vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
if (n > 0)
{
ws_send_binary(conn_id, buf, (size_t)n);
}
}
void async_cmd_complete(const char *module, bool success, const char *msg)
{
if (!module || !msg)
{
return;
}
unsigned long conn_id = ws_get_async_cmd_conn(module);
if (conn_id == 0)
{
return;
}
char buf[1024];
int n = snprintf(buf, sizeof(buf), "[%s] %s %s\n",
module, success ? "OK" : "FAIL", msg);
ws_send_binary(conn_id, buf, (size_t)n);
}
/* ================================================================
* X-Macro
* ================================================================ */
int app_web_server_init1(void *arg)
{
(void)arg;
mg_mgr_init(&s_ws_mgr);
mg_http_listen(&s_ws_mgr, WS_LISTEN_ON, ws_event_handler, NULL);
LOG_I("web_server listening on %s, ws=%s", WS_LISTEN_ON, WS_UPGRADE_URI);
return 0;
}
int app_web_server_init2(void *arg)
{
(void)arg;
return 0;
}
void *app_web_server(void *arg)
{
if (!arg)
{
LOG_E("app_web_server arg null");
return NULL;
}
stru_app *p_app = (stru_app *)arg;
uint32_t event;
uint32_t cnt_1s = 0;
while (1)
{
task_event_recv(p_app->p_event,
EV_TIMER1 | EV_TIMER2 | EV_TIMER3,
TASK_EVENT_FLAG_OR | TASK_EVENT_FLAG_CLEAR,
TASK_EVENT_WAIT_FOREVER,
&event);
if (event & EV_STOP)
{
LOG_I("web_server stopping");
break;
}
if (event & EV_TIMER1)
{
/* IO 轮询(非阻塞) */
mg_mgr_poll(&s_ws_mgr, 0);
}
if (event & EV_TIMER2)
{
/* 增量推送 */
ws_push_task();
}
if (event & EV_TIMER3)
{
cnt_1s++;
p_app->run_cnt++;
}
}
mg_mgr_free(&s_ws_mgr);
LOG_I("web_server stopped, total 1s cycles=%u", cnt_1s);
return NULL;
}

View File

@ -0,0 +1,671 @@
/**
* @file ws_method.cpp
* @brief WebSocket CRUD
* @details JSON {type, saddr, signal_type, curd, signal_data}
* g_ws_sig_methods
* cmd/get_cmds/get_file/set_file
*/
#include "web_server.h"
#include "myLog.h"
#include "myDatacenter.h"
#include "myCmd.h"
#include "myFunc.h"
#include "myMongoose.h"
#include "cJSON.h"
#include <string>
#include <vector>
#include <map>
#include <cstring>
#include <unistd.h>
/* ---- 前向声明(来自 web_server.cpp ---- */
extern std::string ws_cmd_exec(const char *cmd_line);
/* ---- Per-Connection Session ---- */
typedef struct
{
std::string saddr;
std::string desc;
uint8_t data_type;
uint8_t ctrl_type;
std::vector<void *> vec_p_data;
std::string last_val;
} stru_ws_sig;
typedef struct
{
std::vector<stru_ws_sig> signals[WS_SIG_MAX];
} stru_ws_session;
static std::map<unsigned long, stru_ws_session> g_ws_sessions;
static pthread_mutex_t g_ws_session_mutex = PTHREAD_MUTEX_INITIALIZER;
/* ---- 前向声明 ---- */
static int ws_make_sig_json(stru_ws_session *s, cJSON *root, enum_ws_sig_type st, bool *has_change);
static void ws_sig_add(stru_ws_session *s, enum_ws_sig_type st, const char *saddr);
static void ws_sig_del(stru_ws_session *s, enum_ws_sig_type st, const char *saddr);
static int ws_sig_set(stru_ws_session *s, enum_ws_sig_type st, const char *saddr,
uint8_t setting_zone, const char *val);
/* ---- 函数表: 信号类型 → 名称 ---- */
static const char *g_ws_sig_names[WS_SIG_MAX] =
{
"out", "in", "yk", "ao", "param"
};
/* ================================================================
*
* ================================================================ */
static int ws_sig_find_idx(const std::vector<stru_ws_sig> &vec, const char *saddr)
{
for (size_t i = 0; i < vec.size(); i++)
{
if (vec[i].saddr == saddr)
{
return (int)i;
}
}
return -1;
}
static std::string ws_get_val_str(const void *p_data, uint8_t data_type)
{
char buf[128] = {0};
dc_get_signal_val(p_data, data_type, buf, sizeof(buf));
return std::string(buf);
}
/* ================================================================
* out/in/yk/ao/param 5
* ================================================================ */
static void ws_sig_add_out(stru_ws_session *s, const char *saddr)
{
std::vector<stru_ws_sig> &vec = s->signals[WS_SIG_OUT];
void *p_data = NULL;
if (ws_sig_find_idx(vec, saddr) >= 0)
{
return;
}
stru_ws_sig sig;
sig.saddr = saddr;
sig.data_type = 0;
char desc[128] = {0};
if (0 != dc_get_out_signal_info(saddr, desc, sizeof(desc), &sig.data_type, &p_data))
{
LOG_E("dc_get_out_signal_info failed: %s", saddr);
return;
}
sig.desc = desc;
sig.vec_p_data.push_back(p_data);
vec.push_back(sig);
}
static void ws_sig_add_in(stru_ws_session *s, const char *saddr)
{
std::vector<stru_ws_sig> &vec = s->signals[WS_SIG_IN];
void *p_data = NULL;
if (ws_sig_find_idx(vec, saddr) >= 0)
{
return;
}
stru_ws_sig sig;
sig.saddr = saddr;
char desc[128] = {0};
if (0 != dc_get_in_signal_info(saddr, desc, sizeof(desc), &sig.data_type, &p_data))
{
LOG_E("dc_get_in_signal_info failed: %s", saddr);
return;
}
sig.desc = desc;
sig.vec_p_data.push_back(p_data);
vec.push_back(sig);
}
static void ws_sig_add_yk(stru_ws_session *s, const char *saddr)
{
std::vector<stru_ws_sig> &vec = s->signals[WS_SIG_YK];
void *p_data = NULL;
if (ws_sig_find_idx(vec, saddr) >= 0)
{
return;
}
stru_ws_sig sig;
sig.saddr = saddr;
char desc[128] = {0};
if (0 != dc_get_yk_signal_info(saddr, desc, sizeof(desc), &sig.data_type, &sig.ctrl_type, &p_data))
{
LOG_E("dc_get_yk_signal_info failed: %s", saddr);
return;
}
sig.desc = desc;
sig.vec_p_data.push_back(p_data);
vec.push_back(sig);
}
static void ws_sig_add_ao(stru_ws_session *s, const char *saddr)
{
std::vector<stru_ws_sig> &vec = s->signals[WS_SIG_AO];
void *p_data = NULL;
if (ws_sig_find_idx(vec, saddr) >= 0)
{
return;
}
stru_ws_sig sig;
sig.saddr = saddr;
char desc[128] = {0};
if (0 != dc_get_ao_signal_info(saddr, desc, sizeof(desc), &sig.data_type, NULL, &sig.ctrl_type, &p_data))
{
LOG_E("dc_get_ao_signal_info failed: %s", saddr);
return;
}
sig.desc = desc;
sig.vec_p_data.push_back(p_data);
vec.push_back(sig);
}
static void ws_sig_add_param(stru_ws_session *s, const char *saddr)
{
std::vector<stru_ws_sig> &vec = s->signals[WS_SIG_PARAM];
void *p_data_arr[16] = {NULL};
int num_zones = 0;
if (ws_sig_find_idx(vec, saddr) >= 0)
{
return;
}
stru_ws_sig sig;
sig.saddr = saddr;
char desc[128] = {0};
if (0 != dc_get_param_signal_info(saddr, desc, sizeof(desc), &sig.data_type, NULL, &sig.ctrl_type, p_data_arr, &num_zones))
{
LOG_E("dc_get_param_signal_info failed: %s", saddr);
return;
}
sig.desc = desc;
for (int i = 0; i < num_zones; i++)
{
sig.vec_p_data.push_back(p_data_arr[i]);
}
vec.push_back(sig);
}
static void ws_sig_add(stru_ws_session *s, enum_ws_sig_type st, const char *saddr)
{
switch (st)
{
case WS_SIG_OUT: ws_sig_add_out(s, saddr); break;
case WS_SIG_IN: ws_sig_add_in(s, saddr); break;
case WS_SIG_YK: ws_sig_add_yk(s, saddr); break;
case WS_SIG_AO: ws_sig_add_ao(s, saddr); break;
case WS_SIG_PARAM: ws_sig_add_param(s, saddr); break;
default: break;
}
}
static void ws_sig_del(stru_ws_session *s, enum_ws_sig_type st, const char *saddr)
{
std::vector<stru_ws_sig> &vec = s->signals[st];
for (size_t i = 0; i < vec.size(); i++)
{
if (vec[i].saddr == saddr)
{
vec.erase(vec.begin() + i);
return;
}
}
}
static int ws_sig_set(stru_ws_session *s, enum_ws_sig_type st, const char *saddr,
uint8_t setting_zone, const char *val)
{
std::vector<stru_ws_sig> &vec = s->signals[st];
uint8_t data[128] = {0};
for (size_t i = 0; i < vec.size(); i++)
{
if (vec[i].saddr != saddr)
{
continue;
}
dc_set_val_from_str(data, vec[i].data_type, val);
switch (st)
{
case WS_SIG_OUT:
return dc_set_out_signal_val(saddr, data, MODULE_WEB_SERVER);
case WS_SIG_IN:
/* in 信号只读 */
LOG_E("set in signal not supported: %s", saddr);
return -1;
case WS_SIG_YK:
{
dc_ctrl_t ctrl;
ctrl.type = vec[i].ctrl_type;
ctrl.step = DC_STEP_READY;
ctrl.data_type = vec[i].data_type;
ctrl.p_data = dc_create_data(vec[i].data_type);
int rc = dc_signal_yk_set_status(saddr, DC_STEP_DIRECT, &ctrl, data, MODULE_WEB_SERVER);
dc_destroy_data(ctrl.p_data, vec[i].data_type);
return rc;
}
case WS_SIG_AO:
{
dc_ctrl_t ctrl;
ctrl.type = vec[i].ctrl_type;
ctrl.step = DC_STEP_READY;
ctrl.data_type = vec[i].data_type;
ctrl.p_data = dc_create_data(vec[i].data_type);
int rc = dc_signal_ao_set_val(saddr, DC_STEP_DIRECT, &ctrl, data, MODULE_WEB_SERVER);
dc_destroy_data(ctrl.p_data, vec[i].data_type);
return rc;
}
case WS_SIG_PARAM:
{
dc_ctrl_t ctrl;
ctrl.type = vec[i].ctrl_type;
ctrl.step = DC_STEP_READY;
ctrl.data_type = vec[i].data_type;
ctrl.p_data = dc_create_data(vec[i].data_type);
int rc = dc_signal_param_set_val(saddr, DC_STEP_DIRECT, &ctrl, setting_zone, data, MODULE_WEB_SERVER);
dc_destroy_data(ctrl.p_data, vec[i].data_type);
return rc;
}
default:
break;
}
}
LOG_E("signal not found in session: %s", saddr);
return -1;
}
/* ================================================================
* JSON
* ================================================================ */
static int ws_make_sig_json(stru_ws_session *s, cJSON *root, enum_ws_sig_type st, bool *has_change)
{
const std::vector<stru_ws_sig> &vec = s->signals[st];
if (vec.empty())
{
return 0;
}
cJSON *arr = cJSON_CreateArray();
cJSON_AddItemToObject(root, g_ws_sig_names[st], arr);
for (size_t i = 0; i < vec.size(); i++)
{
const stru_ws_sig *sig = &vec[i];
cJSON *item = cJSON_CreateObject();
cJSON_AddStringToObject(item, "saddr", sig->saddr.c_str());
cJSON_AddStringToObject(item, "desc", sig->desc.c_str());
cJSON_AddStringToObject(item, "type", dc_get_type_name(sig->data_type));
/* yk/ao/param: 控制类型 */
if (st == WS_SIG_YK || st == WS_SIG_AO || st == WS_SIG_PARAM)
{
cJSON_AddNumberToObject(item, "ctrl_type", sig->ctrl_type);
}
/* param: 多定值区 */
if (st == WS_SIG_PARAM)
{
cJSON *zone_arr = cJSON_CreateArray();
cJSON_AddItemToObject(item, "setting_zone_list", zone_arr);
for (size_t j = 0; j < sig->vec_p_data.size(); j++)
{
cJSON *zone_item = cJSON_CreateObject();
std::string id = std::to_string(j);
std::string val = ws_get_val_str(sig->vec_p_data[j], sig->data_type);
cJSON_AddStringToObject(zone_item, "id", id.c_str());
cJSON_AddStringToObject(zone_item, "val", val.c_str());
cJSON_AddItemToArray(zone_arr, zone_item);
}
/* 组合值用于变化检测 */
std::string combined;
for (size_t j = 0; j < sig->vec_p_data.size(); j++)
{
combined += ws_get_val_str(sig->vec_p_data[j], sig->data_type);
combined += ",";
}
if (combined != sig->last_val)
{
/* 注意: last_val 是 const 方法访问,这里需要 const_cast */
const_cast<stru_ws_sig *>(sig)->last_val = combined;
*has_change = true;
}
}
else
{
/* out/in/yk/ao: 单值 */
std::string val = ws_get_val_str(sig->vec_p_data[0], sig->data_type);
cJSON_AddStringToObject(item, "val", val.c_str());
if (val != sig->last_val)
{
const_cast<stru_ws_sig *>(sig)->last_val = val;
*has_change = true;
}
}
cJSON_AddItemToArray(arr, item);
}
return 0;
}
/* ================================================================
* EV_TIMER2
* ================================================================ */
void ws_push_task(void)
{
pthread_mutex_lock(&g_ws_session_mutex);
for (auto &pair : g_ws_sessions)
{
unsigned long conn_id = pair.first;
stru_ws_session &sess = pair.second;
bool has_any = false;
for (int st = 0; st < WS_SIG_MAX; st++)
{
if (!sess.signals[st].empty())
{
has_any = true;
break;
}
}
if (!has_any)
{
continue;
}
cJSON *root = cJSON_CreateObject();
bool has_change = false;
for (int st = 0; st < WS_SIG_MAX; st++)
{
ws_make_sig_json(&sess, root, (enum_ws_sig_type)st, &has_change);
}
if (!has_change)
{
cJSON_Delete(root);
continue;
}
char *p_tx = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
if (p_tx)
{
ws_send_one(conn_id, p_tx, strlen(p_tx));
free(p_tx);
}
}
pthread_mutex_unlock(&g_ws_session_mutex);
}
/* ================================================================
* WebSocket JSON
* ================================================================ */
void ws_recv(struct mg_connection *c, const char *p_rx, uint16_t rx_len)
{
if (!p_rx || rx_len == 0)
{
return;
}
cJSON *root = cJSON_Parse(p_rx);
if (!root)
{
LOG_E("ws_recv parse failed: %.200s", p_rx);
return;
}
/* ---- type=cmd: 远程命令执行 ---- */
cJSON *jt = cJSON_GetObjectItem(root, "type");
if (jt && cJSON_IsString(jt) && jt->valuestring)
{
if (0 == strcmp(jt->valuestring, "cmd"))
{
cJSON *jd = cJSON_GetObjectItem(root, "data");
if (jd && cJSON_IsString(jd) && jd->valuestring)
{
/* 提取模块名存异步通道 */
char mod[64] = {0};
const char *pp = jd->valuestring;
int mi = 0;
while (*pp && *pp != ' ' && *pp != '\t' && mi < 63)
{
mod[mi++] = *pp++;
}
ws_set_async_cmd_conn(mod, c->id);
std::string out = ws_cmd_exec(jd->valuestring);
ws_send_binary(c->id, out.data(), out.size());
}
cJSON_Delete(root);
return;
}
/* ---- type=get_cmds: 获取命令列表 ---- */
if (0 == strcmp(jt->valuestring, "get_cmds"))
{
unsigned int count = 0;
stru_cmd *cmds = cmd_manager_get_commands(&count);
cJSON *resp = cJSON_CreateObject();
cJSON_AddStringToObject(resp, "type", "cmd_list");
cJSON *arr = cJSON_CreateArray();
for (unsigned int i = 0; i < count; i++)
{
cJSON *item = cJSON_CreateObject();
cJSON_AddStringToObject(item, "name", cmds[i].name);
cJSON_AddStringToObject(item, "desc", cmds[i].desc);
cJSON_AddItemToArray(arr, item);
}
cJSON_AddItemToObject(resp, "cmds", arr);
char *p_tx = cJSON_PrintUnformatted(resp);
if (p_tx)
{
ws_send_one(c->id, p_tx, strlen(p_tx));
free(p_tx);
}
cJSON_Delete(resp);
cJSON_Delete(root);
return;
}
}
/* ---- 信号 CRUD 命令 ---- */
cJSON *jsaddr = cJSON_GetObjectItem(root, "saddr");
cJSON *jsigtype = cJSON_GetObjectItem(root, "signal_type");
cJSON *jcurd = cJSON_GetObjectItem(root, "curd");
cJSON *jsigdata = cJSON_GetObjectItem(root, "signal_data");
cJSON *jzone = cJSON_GetObjectItem(root, "setting_zone");
if (!jsaddr || !cJSON_IsString(jsaddr) || !jsaddr->valuestring)
{
cJSON_Delete(root);
return;
}
if (!jsigtype || !cJSON_IsString(jsigtype) || !jsigtype->valuestring)
{
cJSON_Delete(root);
return;
}
if (!jcurd || !cJSON_IsString(jcurd) || !jcurd->valuestring)
{
cJSON_Delete(root);
return;
}
/* 查找信号类型 */
enum_ws_sig_type st = WS_SIG_OUT;
for (int i = 0; i < WS_SIG_MAX; i++)
{
if (0 == strcmp(jsigtype->valuestring, g_ws_sig_names[i]))
{
st = (enum_ws_sig_type)i;
break;
}
}
const char *saddr = jsaddr->valuestring;
const char *curd = jcurd->valuestring;
const char *sig_data = jsigdata && cJSON_IsString(jsigdata) ? jsigdata->valuestring : "";
uint8_t zone = jzone && cJSON_IsNumber(jzone) ? (uint8_t)jzone->valueint : 0;
pthread_mutex_lock(&g_ws_session_mutex);
stru_ws_session &sess = g_ws_sessions[c->id];
if (0 == strcmp(curd, "add"))
{
ws_sig_add(&sess, st, saddr);
/* 立即推送当前状态 */
cJSON *push = cJSON_CreateObject();
bool dummy = false;
ws_make_sig_json(&sess, push, st, &dummy);
char *p_tx = cJSON_PrintUnformatted(push);
if (p_tx)
{
ws_send_one(c->id, p_tx, strlen(p_tx));
free(p_tx);
}
cJSON_Delete(push);
}
else if (0 == strcmp(curd, "del"))
{
ws_sig_del(&sess, st, saddr);
cJSON *push = cJSON_CreateObject();
bool dummy = false;
ws_make_sig_json(&sess, push, st, &dummy);
char *p_tx = cJSON_PrintUnformatted(push);
if (p_tx)
{
ws_send_one(c->id, p_tx, strlen(p_tx));
free(p_tx);
}
cJSON_Delete(push);
}
else if (0 == strcmp(curd, "set"))
{
ws_sig_set(&sess, st, saddr, zone, sig_data);
}
pthread_mutex_unlock(&g_ws_session_mutex);
cJSON_Delete(root);
}
/* ---- 会话销毁 ---- */
void ws_session_destroy(struct mg_connection *c)
{
if (!c)
{
return;
}
pthread_mutex_lock(&g_ws_session_mutex);
g_ws_sessions.erase(c->id);
pthread_mutex_unlock(&g_ws_session_mutex);
}