<修改> 1、在webserver增加自定义命令行调试页面
This commit is contained in:
parent
18ee3d8acd
commit
6ef960525c
|
|
@ -1,3 +1,5 @@
|
|||
#include <signal.h>
|
||||
|
||||
#include "myBase.h"
|
||||
#include "myLog.h"
|
||||
#include "myFunc.h"
|
||||
|
|
@ -40,6 +42,8 @@ int app_cfg_parse()
|
|||
|
||||
int main(void)
|
||||
{
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
|
||||
#ifdef RK356x
|
||||
MY_LOG_I("RK356x");
|
||||
#else
|
||||
|
|
|
|||
|
|
@ -2228,8 +2228,12 @@ LOCAL void dc_show_signals(stru_signal_map &dc_signal_map)
|
|||
<< pad_left(val_str, COL_VAL)
|
||||
<< pad_left(link_str, COL_LINK)
|
||||
<< endl;
|
||||
|
||||
if (!cout.good()) break;
|
||||
}
|
||||
|
||||
if (!cout.good()) return;
|
||||
|
||||
cout << endl;
|
||||
|
||||
cout << "signal_id: " << dc_signal_map.signal_id << endl;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@
|
|||
struct mg_connection;
|
||||
|
||||
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_one(unsigned long conn_id, const char *p_tx, uint16_t tx_len);
|
||||
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_session_destroy(struct mg_connection *c);
|
||||
void ws_task(void);
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -18,11 +18,11 @@ LOCAL pthread_mutex_t g_ws_conns_mutex = PTHREAD_MUTEX_INITIALIZER;
|
|||
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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -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)
|
||||
{
|
||||
LOG_E("ws_send_one arg not valid, p_tx:%p, tx_len:%d", p_tx, tx_len);
|
||||
return;
|
||||
LOG_E("ws_send_one arg not valid, p_tx:%p, tx_len:%zu", p_tx, tx_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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@
|
|||
#include "mySystem.h"
|
||||
#include "myDatacenter.h"
|
||||
#include "mongoose.h"
|
||||
#include "myCmd.h"
|
||||
#include <pthread.h>
|
||||
#include <vector>
|
||||
#include <unistd.h>
|
||||
|
||||
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)
|
||||
{
|
||||
|
|
@ -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_data = cJSON_GetObjectItem(root, "signal_data");
|
||||
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");
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -538,3 +538,54 @@ body.login-page #main-content { margin-left: 0; display: flex; align-items: cent
|
|||
.signal-table { font-size: 12px; }
|
||||
.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; }
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@ var App = (function() {
|
|||
{ hash: '#yk', label: '⚡ 遥控 (yk)' },
|
||||
{ hash: '#ao', label: '🔧 参数 (ao)' },
|
||||
{ hash: '#param', label: '⚙ 定值 (param)' },
|
||||
{ hash: '#monitor', label: '📋 数据监控' }
|
||||
{ hash: '#monitor', label: '📋 数据监控' },
|
||||
{ hash: '#terminal', label: '💻 命令终端' }
|
||||
];
|
||||
|
||||
function buildNav() {
|
||||
|
|
@ -137,8 +138,18 @@ var App = (function() {
|
|||
|
||||
/* ---- WS 下行数据分发 ---- */
|
||||
function onWsMessage(data, raw) {
|
||||
if (raw instanceof ArrayBuffer && currentPage && currentPage.onBinary) {
|
||||
currentPage.onBinary(raw);
|
||||
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'];
|
||||
for (var i = 0; i < signalTypes.length; i++) {
|
||||
var st = signalTypes[i];
|
||||
|
|
@ -173,6 +184,7 @@ var App = (function() {
|
|||
registerPage('#ao', AoPage);
|
||||
registerPage('#param', ParamPage);
|
||||
registerPage('#monitor', MonitorPage);
|
||||
registerPage('#terminal', TerminalPage);
|
||||
|
||||
// 构建导航
|
||||
buildNav();
|
||||
|
|
|
|||
|
|
@ -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 > </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() {
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ var WsClient = (function() {
|
|||
|
||||
try {
|
||||
ws = new WebSocket(url);
|
||||
ws.binaryType = 'arraybuffer';
|
||||
} catch (e) {
|
||||
setStatus('disconnected');
|
||||
scheduleReconnect();
|
||||
|
|
@ -57,6 +58,16 @@ var WsClient = (function() {
|
|||
ws.onmessage = function(e) {
|
||||
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') {
|
||||
MonitorRing.push('↓', raw);
|
||||
|
|
|
|||
Loading…
Reference in New Issue