<修改> 1、在webserver增加自定义命令行调试页面

This commit is contained in:
ypc 2026-06-17 19:01:50 +08:00
parent 18ee3d8acd
commit 6ef960525c
10 changed files with 1254 additions and 576 deletions

View File

@ -1,3 +1,5 @@
#include <signal.h>
#include "myBase.h" #include "myBase.h"
#include "myLog.h" #include "myLog.h"
#include "myFunc.h" #include "myFunc.h"
@ -40,6 +42,8 @@ int app_cfg_parse()
int main(void) int main(void)
{ {
signal(SIGPIPE, SIG_IGN);
#ifdef RK356x #ifdef RK356x
MY_LOG_I("RK356x"); MY_LOG_I("RK356x");
#else #else

View File

@ -2228,8 +2228,12 @@ LOCAL void dc_show_signals(stru_signal_map &dc_signal_map)
<< pad_left(val_str, COL_VAL) << pad_left(val_str, COL_VAL)
<< pad_left(link_str, COL_LINK) << pad_left(link_str, COL_LINK)
<< endl; << endl;
if (!cout.good()) break;
} }
if (!cout.good()) return;
cout << endl; cout << endl;
cout << "signal_id: " << dc_signal_map.signal_id << endl; cout << "signal_id: " << dc_signal_map.signal_id << endl;

View File

@ -7,7 +7,8 @@
struct mg_connection; struct mg_connection;
void ws_recv(struct mg_connection *c, const char *p_rx, uint16_t rx_len); void ws_recv(struct mg_connection *c, const char *p_rx, uint16_t rx_len);
void ws_send_all(const char *p_tx, uint16_t tx_len); void ws_send_all(const char *p_tx, size_t tx_len);
void ws_send_one(unsigned long conn_id, const char *p_tx, uint16_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_session_destroy(struct mg_connection *c); void ws_session_destroy(struct mg_connection *c);
void ws_task(void); void ws_task(void);

File diff suppressed because it is too large Load Diff

View File

@ -18,11 +18,11 @@ LOCAL pthread_mutex_t g_ws_conns_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_t thread_id; static pthread_t thread_id;
void ws_send_all(const char *p_tx, uint16_t tx_len) void ws_send_all(const char *p_tx, size_t tx_len)
{ {
if(NULL == p_tx || 0 == tx_len) if(NULL == p_tx || 0 == tx_len)
{ {
LOG_E("ws_send_all arg not valid, p_tx:%p, tx_len:%d", p_tx, tx_len); LOG_E("ws_send_all arg not valid, p_tx:%p, tx_len:%zu", p_tx, tx_len);
return; return;
} }
@ -39,25 +39,48 @@ void ws_send_all(const char *p_tx, uint16_t tx_len)
} }
void ws_send_one(unsigned long conn_id, const char *p_tx, uint16_t tx_len) bool ws_send_one(unsigned long conn_id, const char *p_tx, size_t tx_len)
{ {
if(NULL == p_tx || 0 == tx_len) if(NULL == p_tx || 0 == tx_len)
{ {
LOG_E("ws_send_one arg not valid, p_tx:%p, tx_len:%d", p_tx, tx_len); LOG_E("ws_send_one arg not valid, p_tx:%p, tx_len:%zu", p_tx, tx_len);
return; return false;
} }
bool ok = false;
pthread_mutex_lock(&g_ws_conns_mutex); pthread_mutex_lock(&g_ws_conns_mutex);
for(uint32_t i = 0; i < g_ws_conns.size(); i++) for(uint32_t i = 0; i < g_ws_conns.size(); i++)
{ {
struct mg_connection *c = g_ws_conns[i]; struct mg_connection *c = g_ws_conns[i];
if(c->id == conn_id && c->is_websocket && !c->is_draining) if(c->id == conn_id && c->is_websocket && !c->is_draining)
{ {
mg_ws_send(c, p_tx, tx_len, WEBSOCKET_OP_TEXT); size_t sent = mg_ws_send(c, p_tx, tx_len, WEBSOCKET_OP_TEXT);
ok = (sent >= tx_len);
break; break;
} }
} }
pthread_mutex_unlock(&g_ws_conns_mutex); pthread_mutex_unlock(&g_ws_conns_mutex);
return ok;
}
bool ws_send_binary(unsigned long conn_id, const void *data, size_t len)
{
if(NULL == data || 0 == len) return false;
bool ok = false;
pthread_mutex_lock(&g_ws_conns_mutex);
for(uint32_t i = 0; i < g_ws_conns.size(); i++)
{
struct 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;
} }

View File

@ -4,6 +4,10 @@
#include "mySystem.h" #include "mySystem.h"
#include "myDatacenter.h" #include "myDatacenter.h"
#include "mongoose.h" #include "mongoose.h"
#include "myCmd.h"
#include <pthread.h>
#include <vector>
#include <unistd.h>
typedef struct typedef struct
{ {
@ -499,6 +503,148 @@ LOCAL void set_param_signal_data(stru_ws_session &s, const std::string& saddr, c
} }
LOCAL void ws_send_large_text(unsigned long conn_id, const char *type, const std::string &data)
{
const size_t CHUNK_MAX = 35000;
if(data.size() <= CHUNK_MAX)
{
cJSON *resp = cJSON_CreateObject();
cJSON_AddStringToObject(resp, "type", type);
cJSON_AddStringToObject(resp, "data", data.c_str());
char *p_tx = cJSON_PrintUnformatted(resp);
if(p_tx)
{
ws_send_one(conn_id, p_tx, strlen(p_tx));
free(p_tx);
}
cJSON_Delete(resp);
return;
}
std::vector<size_t> cuts;
size_t pos = 0;
while(pos < data.size())
{
size_t end = pos + CHUNK_MAX;
if(end >= data.size()) end = data.size();
while(end < data.size() && (data[end] & 0xC0) == 0x80) end++;
cuts.push_back(end);
pos = end;
}
size_t total_chunks = cuts.size();
pos = 0;
MY_LOG_I("ws_send_large_text: %zu chunks, %zu bytes total", total_chunks, data.size());
for(size_t seq = 0; seq < total_chunks; seq++)
{
size_t end = cuts[seq];
std::string chunk = data.substr(pos, end - pos);
pos = end;
cJSON *resp = cJSON_CreateObject();
cJSON_AddStringToObject(resp, "type", "cmd_rsp_chunk");
cJSON_AddNumberToObject(resp, "seq", (double)seq);
cJSON_AddNumberToObject(resp, "total", (double)total_chunks);
cJSON_AddStringToObject(resp, "data", chunk.c_str());
char *p_tx = cJSON_PrintUnformatted(resp);
if(p_tx)
{
size_t tx_len = strlen(p_tx);
bool ok = ws_send_one(conn_id, p_tx, tx_len);
free(p_tx);
if(!ok)
{
MY_LOG_E("ws_send_large_text: chunk %zu/%zu failed (len=%zu)", seq, total_chunks, tx_len);
cJSON_Delete(resp);
break;
}
usleep(5000);
}
cJSON_Delete(resp);
}
}
LOCAL std::string ws_cmd_exec(const char *cmd_line)
{
if(NULL == cmd_line || 0 == strlen(cmd_line))
{
return "";
}
char line[1024];
strncpy(line, cmd_line, sizeof(line) - 1);
line[sizeof(line) - 1] = '\0';
char *argv[16];
int argc = 0;
char *p = strtok(line, " \t");
while(NULL != p && argc < 16)
{
argv[argc++] = p;
p = strtok(NULL, " \t");
}
if(0 == argc)
{
return "";
}
stru_cmd *p_cmd = cmd_find(argv[0]);
if(NULL == p_cmd)
{
return std::string("[unknown] ") + argv[0];
}
fflush(stdout);
int pipefd[2];
if(0 != pipe(pipefd))
{
return "pipe failed";
}
std::string result;
pthread_t reader_thread;
auto *reader_ctx = new std::pair<int, std::string*>(pipefd[0], &result);
pthread_create(&reader_thread, NULL,
[](void *arg) -> void* {
auto *ctx = (std::pair<int, std::string*>*)arg;
char rbuf[4096];
ssize_t nr;
while((nr = read(ctx->first, rbuf, sizeof(rbuf))) > 0)
{
ctx->second->append(rbuf, nr);
}
close(ctx->first);
delete ctx;
return NULL;
},
reader_ctx);
int saved_stdout = dup(STDOUT_FILENO);
dup2(pipefd[1], STDOUT_FILENO);
close(pipefd[1]);
p_cmd->func(argc, argv);
fflush(stdout);
dup2(saved_stdout, STDOUT_FILENO);
close(saved_stdout);
pthread_join(reader_thread, NULL);
if(!result.empty())
{
return result;
}
return "(no output)";
}
void ws_recv(struct mg_connection *c, const char* p_rx, uint16_t rx_len) void ws_recv(struct mg_connection *c, const char* p_rx, uint16_t rx_len)
{ {
@ -526,6 +672,22 @@ void ws_recv(struct mg_connection *c, const char* p_rx, uint16_t rx_len)
cJSON *signal_type = cJSON_GetObjectItem(root, "signal_type"); cJSON *signal_type = cJSON_GetObjectItem(root, "signal_type");
cJSON *signal_data = cJSON_GetObjectItem(root, "signal_data"); cJSON *signal_data = cJSON_GetObjectItem(root, "signal_data");
cJSON *setting_zone = cJSON_GetObjectItem(root, "setting_zone"); cJSON *setting_zone = cJSON_GetObjectItem(root, "setting_zone");
cJSON *type = cJSON_GetObjectItem(root, "type");
if(type && cJSON_IsString(type) && type->valuestring)
{
if(0 == strcmp(type->valuestring, "cmd"))
{
cJSON *data = cJSON_GetObjectItem(root, "data");
if(data && cJSON_IsString(data) && data->valuestring)
{
std::string result = ws_cmd_exec(data->valuestring);
ws_send_binary(c->id, result.data(), result.size());
}
cJSON_Delete(root);
return;
}
}
cJSON *curd = cJSON_GetObjectItem(root, "curd"); cJSON *curd = cJSON_GetObjectItem(root, "curd");

View File

@ -538,3 +538,54 @@ body.login-page #main-content { margin-left: 0; display: flex; align-items: cent
.signal-table { font-size: 12px; } .signal-table { font-size: 12px; }
.signal-table thead th, .signal-table tbody td { padding: 6px 8px; } .signal-table thead th, .signal-table tbody td { padding: 6px 8px; }
} }
/* ===== 命令终端 ===== */
.terminal-wrap {
background: #1e1e1e;
border-radius: 4px;
overflow: hidden;
display: flex;
flex-direction: column;
height: calc(100vh - 200px);
min-height: 400px;
}
.terminal-output {
flex: 1;
overflow-y: auto;
padding: 10px;
font-family: 'Courier New', monospace;
font-size: 13px;
color: #d4d4d4;
white-space: pre-wrap;
word-break: break-all;
background: #1e1e1e;
}
.terminal-output div {
line-height: 1.5;
}
.terminal-input-line {
display: flex;
background: #2d2d2d;
padding: 6px 10px;
border-top: 1px solid #3c3c3c;
}
.terminal-prompt {
color: #6a9955;
padding: 4px 6px 4px 0;
font-family: 'Courier New', monospace;
font-size: 13px;
white-space: nowrap;
}
.terminal-input {
flex: 1;
background: transparent;
border: none;
color: #d4d4d4;
font-family: 'Courier New', monospace;
font-size: 13px;
outline: none;
}
.term-sent { color: #569cd6; }
.term-resp { color: #d4d4d4; }
.term-err { color: #f44747; }
.term-info { color: #6a9955; }

View File

@ -20,7 +20,8 @@ var App = (function() {
{ hash: '#yk', label: '⚡ 遥控 (yk)' }, { hash: '#yk', label: '⚡ 遥控 (yk)' },
{ hash: '#ao', label: '🔧 参数 (ao)' }, { hash: '#ao', label: '🔧 参数 (ao)' },
{ hash: '#param', label: '⚙ 定值 (param)' }, { hash: '#param', label: '⚙ 定值 (param)' },
{ hash: '#monitor', label: '📋 数据监控' } { hash: '#monitor', label: '📋 数据监控' },
{ hash: '#terminal', label: '💻 命令终端' }
]; ];
function buildNav() { function buildNav() {
@ -137,8 +138,18 @@ var App = (function() {
/* ---- WS 下行数据分发 ---- */ /* ---- WS 下行数据分发 ---- */
function onWsMessage(data, raw) { function onWsMessage(data, raw) {
if (raw instanceof ArrayBuffer && currentPage && currentPage.onBinary) {
currentPage.onBinary(raw);
return;
}
if (!data || typeof data !== 'object') return; if (!data || typeof data !== 'object') return;
if (data.type === 'cmd_rsp' && currentPage && currentPage.onData) {
currentPage.onData(data.data);
return;
}
var signalTypes = ['out', 'in', 'yk', 'ao', 'param']; var signalTypes = ['out', 'in', 'yk', 'ao', 'param'];
for (var i = 0; i < signalTypes.length; i++) { for (var i = 0; i < signalTypes.length; i++) {
var st = signalTypes[i]; var st = signalTypes[i];
@ -173,6 +184,7 @@ var App = (function() {
registerPage('#ao', AoPage); registerPage('#ao', AoPage);
registerPage('#param', ParamPage); registerPage('#param', ParamPage);
registerPage('#monitor', MonitorPage); registerPage('#monitor', MonitorPage);
registerPage('#terminal', TerminalPage);
// 构建导航 // 构建导航
buildNav(); buildNav();

View File

@ -684,3 +684,91 @@ var MonitorPage = (function() {
} }
}; };
})(); })();
/* ========================================================================
* 命令终端页面
* ======================================================================== */
var TerminalPage = (function() {
var history = [];
var historyIdx = 0;
function append(text, cls) {
var el = document.getElementById('term-output');
if (!el) return;
var div = document.createElement('div');
div.className = cls || '';
div.textContent = text;
el.appendChild(div);
el.scrollTop = el.scrollHeight;
}
return {
render: function(container) {
history = [];
historyIdx = 0;
container.innerHTML = ''
+ '<div class="page-header"><h2>命令终端</h2><p>输入 RTU 命令Enter 发送,↑↓ 历史记录</p></div>'
+ '<div class="terminal-wrap">'
+ '<div class="terminal-output" id="term-output"></div>'
+ '<div class="terminal-input-line">'
+ '<span class="terminal-prompt">cmd &gt; </span>'
+ '<input type="text" class="terminal-input" id="term-input" autofocus spellcheck="false">'
+ '</div>'
+ '</div>';
var input = document.getElementById('term-input');
if (!input) return;
input.onkeydown = function(e) {
if (e.key === 'Enter') {
var cmd = input.value.trim();
if (cmd) {
history.push(cmd);
historyIdx = history.length;
append('cmd > ' + cmd, 'term-sent');
if (!WsClient.send({type:'cmd', data:cmd})) {
append('[未连接]', 'term-err');
}
}
input.value = '';
} else if (e.key === 'ArrowUp') {
e.preventDefault();
if (history.length > 0) {
historyIdx = Math.max(0, historyIdx - 1);
input.value = history[historyIdx];
}
} else if (e.key === 'ArrowDown') {
e.preventDefault();
if (historyIdx < history.length - 1) {
historyIdx++;
input.value = history[historyIdx];
} else {
historyIdx = history.length;
input.value = '';
}
}
};
append('输入 help 查看所有命令', 'term-info');
},
onData: function(data) {
if (data) append(data, 'term-resp');
},
onBinary: function(ab) {
var decoder = new TextDecoder('utf-8');
var text = decoder.decode(ab);
if (text) append(text, 'term-resp');
},
onEnter: function() {
var input = document.getElementById('term-input');
if (input) setTimeout(function() { input.focus(); }, 100);
},
onLeave: function() {
}
};
})();

View File

@ -30,6 +30,7 @@ var WsClient = (function() {
try { try {
ws = new WebSocket(url); ws = new WebSocket(url);
ws.binaryType = 'arraybuffer';
} catch (e) { } catch (e) {
setStatus('disconnected'); setStatus('disconnected');
scheduleReconnect(); scheduleReconnect();
@ -57,6 +58,16 @@ var WsClient = (function() {
ws.onmessage = function(e) { ws.onmessage = function(e) {
var raw = e.data; var raw = e.data;
if (raw instanceof ArrayBuffer) {
if (typeof MonitorRing !== 'undefined') {
MonitorRing.push('↓', '[binary ' + raw.byteLength + 'B]');
}
for (var i = 0; i < msgCallbacks.length; i++) {
try { msgCallbacks[i](null, raw); } catch (ex) {}
}
return;
}
// 写入监控日志 // 写入监控日志
if (typeof MonitorRing !== 'undefined') { if (typeof MonitorRing !== 'undefined') {
MonitorRing.push('↓', raw); MonitorRing.push('↓', raw);