Skip to content

Commit e4eacb4

Browse files
committed
feat: 添加脚本插件功能,支持 JavaScript 脚本执行
- 在工作台页面中新增脚本插件部分,允许用户输入和管理 JavaScript 脚本。 - 实现脚本的加载、保存、运行和停止功能,并提供日志输出。 - 更新 README 和功能规划文档,标注脚本插件功能已实现。 - 增强多语言支持,添加脚本相关翻译条目。
1 parent 8ef7768 commit e4eacb4

15 files changed

Lines changed: 1158 additions & 12 deletions

CMakeLists.txt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ if(MINGW)
3939
string(APPEND CMAKE_EXE_LINKER_FLAGS_MINSIZEREL " -Wl,--gc-sections -s")
4040
endif()
4141

42-
find_package(Qt6 6.5 REQUIRED COMPONENTS Core Gui Widgets SerialPort Svg Network Core5Compat)
42+
find_package(Qt6 6.5 REQUIRED COMPONENTS Core Gui Widgets SerialPort Svg Network Core5Compat Qml)
4343

4444
set(FLUENTQTWIDGETS_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/third_party/FluentQtWidgets" CACHE PATH "Path to FluentQtWidgets checkout")
4545
if(NOT EXISTS "${FLUENTQTWIDGETS_ROOT}/CMakeLists.txt")
@@ -67,6 +67,8 @@ add_executable(FluentSerialAssistant
6767
src/app/core/hex_utils.h
6868
src/app/core/modbus_utils.cpp
6969
src/app/core/modbus_utils.h
70+
src/app/core/script_runner.cpp
71+
src/app/core/script_runner.h
7072
src/app/core/text_encoding.cpp
7173
src/app/core/text_encoding.h
7274
src/app/core/update_checker.cpp
@@ -98,6 +100,7 @@ add_executable(FluentSerialAssistant
98100
src/app/view/workbench/workbench_page_modbus.cpp
99101
src/app/view/workbench/workbench_page_packets.cpp
100102
src/app/view/workbench/workbench_page_plot.cpp
103+
src/app/view/workbench/workbench_page_scripts.cpp
101104
src/app/view/workbench/workbench_page_side_sections.cpp
102105
src/app/view/workbench/workbench_page_state.cpp
103106
src/app/view/workbench/workbench_page_terminal.cpp
@@ -132,6 +135,7 @@ target_link_libraries(FluentSerialAssistant
132135
Qt6::SerialPort
133136
Qt6::Network
134137
Qt6::Core5Compat
138+
Qt6::Qml
135139
FluentQtWidgets::Widgets
136140
)
137141

README.md

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
Fluent 串口助手是一个基于 C++17、Qt 6 Widgets 和 FluentQtWidgets 的跨平台串口调试工具。项目目标是提供一个现代 Fluent 风格的串口终端,覆盖常用串口连接、收发、显示、导出和主题配置工作流。
1515

16-
当前版本仍处于早期开发阶段,功能以多标签终端工作台、自动日志、快速绘图、数据表格和设置为主
16+
当前版本仍处于早期开发阶段,功能以多标签终端工作台、自动日志、快速绘图、数据表格、脚本插件和设置为主
1717

1818
## 界面截图
1919

@@ -42,6 +42,7 @@ Fluent 串口助手是一个基于 C++17、Qt 6 Widgets 和 FluentQtWidgets 的
4242
- 发送历史:保留最近 20 条唯一发送记录。
4343
- 常用包:支持分组、备注、启用/停用、保存、载入、发送、批量发送、删除、排序和 JSON 导入导出。
4444
- 宏命令:支持多步骤发送、步骤间延时、等待文本/HEX 响应、循环运行、失败中止和 CSV 结果导出。
45+
- 脚本插件:内置 JavaScript 脚本运行器,可通过受控 `serial` API 发送文本/HEX、读取收发记录快照、写脚本日志,并支持停止和异常提示。
4546
- 自动应答:支持文本、HEX 和正则匹配规则,命中后可立即或延时发送指定文本/HEX 应答,并在发送记录中标记来源。
4647
- 循环发送:支持毫秒级发送间隔。
4748
- 文件发送:支持选择文件后按块发送,可配置块大小和块间隔,并显示进度。
@@ -66,7 +67,7 @@ Fluent 串口助手是一个基于 C++17、Qt 6 Widgets 和 FluentQtWidgets 的
6667

6768
暂未开放:
6869

69-
- 插件系统
70+
- 通用插件系统
7071

7172
## 后续规划
7273

@@ -79,6 +80,7 @@ Fluent 串口助手是一个基于 C++17、Qt 6 Widgets 和 FluentQtWidgets 的
7980
- Qt Widgets
8081
- Qt SerialPort
8182
- Qt Svg
83+
- Qt Qml
8284
- FluentQtWidgets
8385
- CMake
8486
- Ninja
@@ -102,6 +104,7 @@ Fluent 串口助手是一个基于 C++17、Qt 6 Widgets 和 FluentQtWidgets 的
102104
│ ├── *_terminal.cpp # 终端记录、过滤、分色和渲染
103105
│ ├── *_packets.cpp # 发送历史和常用包
104106
│ ├── *_macros.cpp # 宏命令和测试序列
107+
│ ├── *_scripts.cpp # JavaScript 脚本插件运行
105108
│ ├── *_auto_reply.cpp # 自动应答规则匹配、发送和持久化
106109
│ ├── *_files.cpp # 记录导出、接收保存和文件发送
107110
│ ├── *_connection.cpp # 连接、重连和控件启停
@@ -133,7 +136,7 @@ git submodule update --init --recursive
133136

134137
- CMake 3.21+
135138
- Ninja
136-
- Qt 6.5+,需要包含 `SerialPort``Svg``Core5Compat` 模块
139+
- Qt 6.5+,需要包含 `SerialPort``Svg``Core5Compat``Qml` 模块
137140
- Windows 本地预设默认使用:
138141
- Qt:`C:/Qt/6.11.1/mingw_64`
139142
- MinGW:`C:/Qt/Tools/mingw1310_64`
@@ -200,14 +203,15 @@ dist/FluentSerialAssistant-release
200203
5. 在发送区输入文本或 HEX 数据,必要时选择收发编码、行结束符和校验自动追加后发送。
201204
6. 可在常用包中填写分组、名称、备注、内容、模式和换行后保存,之后选中条目即可填入发送区、直接发送、批量发送或 JSON 导入导出。
202205
7. 可在宏命令中保存多步骤测试序列,配置延时、期望响应、循环次数和失败中止后运行,并导出 CSV 结果。
203-
8. 可在自动应答中保存文本、HEX 或正则匹配规则,命中后立即或延时发送指定应答。
204-
9. 可选择文件,配置块大小和间隔后分块发送。
205-
10. 可打开快速绘图窗口,按全部数字、分隔值、键值对或 JSON 对象协议从接收文本取数并导出 CSV。
206-
11. 可打开数据表格窗口,按帧排序、过滤、复制记录,并定位回终端对应记录。
207-
12. 可新增标签页同时调试多个串口,新会话会复制当前配置。
208-
13. 可点击左侧卡片标题折叠不常用配置,减少侧栏占用。
209-
14. 可开启自动断帧、时间戳、自动滚动、循环发送和自动日志等选项。
210-
15. 使用 TXT、CSV、BIN 导出会话记录。
206+
8. 可在脚本插件中载入或编写 JavaScript,通过 `serial.sendText()``serial.sendHex()``serial.records()``serial.log()` 自动化调试流程。
207+
9. 可在自动应答中保存文本、HEX 或正则匹配规则,命中后立即或延时发送指定应答。
208+
10. 可选择文件,配置块大小和间隔后分块发送。
209+
11. 可打开快速绘图窗口,按全部数字、分隔值、键值对或 JSON 对象协议从接收文本取数并导出 CSV。
210+
12. 可打开数据表格窗口,按帧排序、过滤、复制记录,并定位回终端对应记录。
211+
13. 可新增标签页同时调试多个串口,新会话会复制当前配置。
212+
14. 可点击左侧卡片标题折叠不常用配置,减少侧栏占用。
213+
15. 可开启自动断帧、时间戳、自动滚动、循环发送和自动日志等选项。
214+
16. 使用 TXT、CSV、BIN 导出会话记录。
211215

212216
## 许可证
213217

docs/feature-roadmap.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,8 @@
247247

248248
### 13. 脚本插件能力
249249

250+
状态:已实现。左侧新增脚本插件卡片,支持 JavaScript 脚本载入、保存、示例、运行、停止和日志输出;脚本通过 `serial` 对象发送文本/HEX、读取收发记录快照和写日志,执行在独立线程中,异常会记录并通过提示显示,不影响主界面和串口连接。
251+
250252
为复杂调试流程预留扩展空间。
251253

252254
需求:

src/app/core/script_runner.cpp

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
#include "app/core/script_runner.h"
2+
3+
#include "app/core/app_i18n.h"
4+
5+
#include <QtCore/QElapsedTimer>
6+
#include <QtCore/QMutexLocker>
7+
#include <QtCore/QThread>
8+
#include <QtQml/QJSEngine>
9+
#include <QtQml/QJSValue>
10+
11+
#include <utility>
12+
13+
namespace {
14+
15+
QString normalizedLineEnding(QString value)
16+
{
17+
value = value.trimmed().toLower();
18+
if (value == QStringLiteral("cr") || value == QStringLiteral("lf") || value == QStringLiteral("crlf")) {
19+
return value;
20+
}
21+
return QStringLiteral("none");
22+
}
23+
24+
} // namespace
25+
26+
ScriptBridge::ScriptBridge(QObject *parent) : QObject(parent) {}
27+
28+
void ScriptBridge::setSendTextCallback(SendCallback callback) { m_sendTextCallback = std::move(callback); }
29+
30+
void ScriptBridge::setSendHexCallback(SendCallback callback) { m_sendHexCallback = std::move(callback); }
31+
32+
void ScriptBridge::setReceivedTextCallback(StringCallback callback) { m_receivedTextCallback = std::move(callback); }
33+
34+
void ScriptBridge::setReceivedHexCallback(StringCallback callback) { m_receivedHexCallback = std::move(callback); }
35+
36+
void ScriptBridge::setLastRxTextCallback(StringCallback callback) { m_lastRxTextCallback = std::move(callback); }
37+
38+
void ScriptBridge::setLastRxHexCallback(StringCallback callback) { m_lastRxHexCallback = std::move(callback); }
39+
40+
void ScriptBridge::setRecordsCallback(RecordsCallback callback) { m_recordsCallback = std::move(callback); }
41+
42+
void ScriptBridge::setStopCallback(StopCallback callback) { m_stopCallback = std::move(callback); }
43+
44+
bool ScriptBridge::sendText(const QString &payload, const QString &lineEnding)
45+
{
46+
return sendWithCallback(m_sendTextCallback, payload, lineEnding);
47+
}
48+
49+
bool ScriptBridge::sendHex(const QString &payload, const QString &lineEnding)
50+
{
51+
return sendWithCallback(m_sendHexCallback, payload, lineEnding);
52+
}
53+
54+
QString ScriptBridge::receivedText() const { return stringFromCallback(m_receivedTextCallback); }
55+
56+
QString ScriptBridge::receivedHex() const { return stringFromCallback(m_receivedHexCallback); }
57+
58+
QString ScriptBridge::lastRxText() const { return stringFromCallback(m_lastRxTextCallback); }
59+
60+
QString ScriptBridge::lastRxHex() const { return stringFromCallback(m_lastRxHexCallback); }
61+
62+
QVariantList ScriptBridge::records() const { return m_recordsCallback ? m_recordsCallback() : QVariantList(); }
63+
64+
void ScriptBridge::log(const QString &message)
65+
{
66+
m_lastError.clear();
67+
emit logMessage(message);
68+
}
69+
70+
void ScriptBridge::sleep(int milliseconds)
71+
{
72+
const int duration = qBound(0, milliseconds, 600000);
73+
QElapsedTimer timer;
74+
timer.start();
75+
while (!stopRequested() && timer.elapsed() < duration) {
76+
const int remaining = duration - static_cast<int>(timer.elapsed());
77+
QThread::msleep(static_cast<unsigned long>(qMin(remaining, 50)));
78+
}
79+
}
80+
81+
bool ScriptBridge::stopRequested() const { return m_stopCallback ? m_stopCallback() : false; }
82+
83+
QString ScriptBridge::lastError() const { return m_lastError; }
84+
85+
bool ScriptBridge::sendWithCallback(const SendCallback &callback, const QString &payload, const QString &lineEnding)
86+
{
87+
if (stopRequested()) {
88+
m_lastError = AppI18n::text("脚本已停止");
89+
emit logMessage(QStringLiteral("[ERROR] %1").arg(m_lastError));
90+
return false;
91+
}
92+
if (!callback) {
93+
m_lastError = AppI18n::text("脚本发送接口不可用");
94+
emit logMessage(QStringLiteral("[ERROR] %1").arg(m_lastError));
95+
return false;
96+
}
97+
98+
QString error;
99+
const bool ok = callback(payload, normalizedLineEnding(lineEnding), &error);
100+
if (!ok) {
101+
m_lastError = error.isEmpty() ? AppI18n::text("发送失败") : error;
102+
emit logMessage(QStringLiteral("[ERROR] %1").arg(m_lastError));
103+
return false;
104+
}
105+
m_lastError.clear();
106+
return true;
107+
}
108+
109+
QString ScriptBridge::stringFromCallback(const StringCallback &callback) const { return callback ? callback() : QString(); }
110+
111+
ScriptRunner::ScriptRunner(QObject *parent) : QObject(parent) {}
112+
113+
ScriptRunner::~ScriptRunner() { requestStop(); }
114+
115+
void ScriptRunner::setSendTextCallback(ScriptBridge::SendCallback callback) { m_sendTextCallback = std::move(callback); }
116+
117+
void ScriptRunner::setSendHexCallback(ScriptBridge::SendCallback callback) { m_sendHexCallback = std::move(callback); }
118+
119+
void ScriptRunner::setReceivedTextCallback(ScriptBridge::StringCallback callback)
120+
{
121+
m_receivedTextCallback = std::move(callback);
122+
}
123+
124+
void ScriptRunner::setReceivedHexCallback(ScriptBridge::StringCallback callback)
125+
{
126+
m_receivedHexCallback = std::move(callback);
127+
}
128+
129+
void ScriptRunner::setLastRxTextCallback(ScriptBridge::StringCallback callback)
130+
{
131+
m_lastRxTextCallback = std::move(callback);
132+
}
133+
134+
void ScriptRunner::setLastRxHexCallback(ScriptBridge::StringCallback callback)
135+
{
136+
m_lastRxHexCallback = std::move(callback);
137+
}
138+
139+
void ScriptRunner::setRecordsCallback(ScriptBridge::RecordsCallback callback) { m_recordsCallback = std::move(callback); }
140+
141+
void ScriptRunner::requestStop()
142+
{
143+
m_stopRequested.store(true);
144+
QMutexLocker locker(&m_engineMutex);
145+
if (m_engine) {
146+
m_engine->setInterrupted(true);
147+
}
148+
}
149+
150+
void ScriptRunner::runScript(const QString &script, const QString &fileName)
151+
{
152+
m_stopRequested.store(false);
153+
emit started();
154+
155+
if (script.trimmed().isEmpty()) {
156+
emit finished(false, AppI18n::text("脚本为空"));
157+
return;
158+
}
159+
160+
QJSEngine engine;
161+
{
162+
QMutexLocker locker(&m_engineMutex);
163+
m_engine = &engine;
164+
}
165+
166+
ScriptBridge bridge;
167+
bridge.setSendTextCallback(m_sendTextCallback);
168+
bridge.setSendHexCallback(m_sendHexCallback);
169+
bridge.setReceivedTextCallback(m_receivedTextCallback);
170+
bridge.setReceivedHexCallback(m_receivedHexCallback);
171+
bridge.setLastRxTextCallback(m_lastRxTextCallback);
172+
bridge.setLastRxHexCallback(m_lastRxHexCallback);
173+
bridge.setRecordsCallback(m_recordsCallback);
174+
bridge.setStopCallback([this]() { return stopRequested(); });
175+
connect(&bridge, &ScriptBridge::logMessage, this, &ScriptRunner::logMessage);
176+
177+
engine.globalObject().setProperty(QStringLiteral("serial"), engine.newQObject(&bridge));
178+
179+
QStringList stackTrace;
180+
const QString effectiveName = fileName.trimmed().isEmpty() ? QStringLiteral("script.js") : fileName;
181+
const QJSValue result = engine.evaluate(script, effectiveName, 1, &stackTrace);
182+
{
183+
QMutexLocker locker(&m_engineMutex);
184+
m_engine = nullptr;
185+
}
186+
187+
if (stopRequested()) {
188+
emit finished(false, AppI18n::text("脚本已停止"));
189+
return;
190+
}
191+
if (result.isError()) {
192+
emit finished(false, errorText(result, stackTrace));
193+
return;
194+
}
195+
196+
emit finished(true, AppI18n::text("脚本执行完成"));
197+
}
198+
199+
bool ScriptRunner::stopRequested() const { return m_stopRequested.load(); }
200+
201+
QString ScriptRunner::errorText(const QJSValue &value, const QStringList &stackTrace) const
202+
{
203+
const int line = value.property(QStringLiteral("lineNumber")).toInt();
204+
QString message = line > 0 ? AppI18n::text("第 %1 行:%2").arg(line).arg(value.toString()) : value.toString();
205+
if (!stackTrace.isEmpty()) {
206+
message.append(QStringLiteral("\n"));
207+
message.append(stackTrace.join(QLatin1Char('\n')));
208+
}
209+
return message;
210+
}

0 commit comments

Comments
 (0)