Update linux macos#221
Conversation
本次提交在不影响原有 WPF (.NET Framework 4.6.2) Windows 版本的前提下,
新增基于 Avalonia 11 + .NET 8 的跨平台方案,使 LLCOM 可运行于 Linux/macOS。
变更概要:
### Rust 串口监听库 (serial_monitor_rs)
- 重构 lib.rs 为平台分派,提取 platform/{windows,unix}.rs
- Windows 保留完整 DLL 注入监控能力
- Linux/macOS 提供 stub 实现(API 调用返回不支持)
- build.rs 支持跨平台构建
- 新增 build.sh Linux/macOS 构建脚本
### .NET 8 共享类库 (llcom.Core/)
- 提取核心业务逻辑模型:Settings, ToSendData, LinePlotPoint, OnlineScript
- 创建跨平台抽象层:PlatformHelper, UartManager, NativeInterop
- 工具类:ByteConvert, Logger, Convert 编码转换
### Avalonia 跨平台 UI (llcom.Avalonia/)
- 基于 Avalonia 11 MVVM 的完整串口调试工具界面
- 串口扫描、打开/关闭、发送、接收、Hex/ASCII 模式切换
- 状态栏显示平台、串口连接状态等
### 其他
- 新增跨平台解决方案 llcom-cross.sln
- 新增 CI/CD 支持 Linux/macOS/Windows 三平台构建
- 更新 .gitignore 排除 .codebuddy/ 和 .slnx 文件
- Linux x64 原生库预编译至 llcom/native/linux-x64/
验证:
- dotnet build llcom-cross.sln: 0 警告 0 错误
- cargo build --release -p serial_monitor: Linux .so 构建成功
- 原 WPF 项目 llcom/ 未做任何修改,Windows 构建不受影响
本次提交将原 WPF 项目的核心页面迁移到 Avalonia 跨平台框架, 并完成构建验证(0 警告 0 错误),原有 Windows WPF 项目不受影响。 - ConvertPageViewModel: 15 种编码转换器(Hex/Base64/URL/HTML/Unicode/MD5/SHA) - EncodingFixViewModel: 6 种编码乱码修复工具 - MqttViewModel: MQTT 客户端(订阅/发布/TLS/WebSocket) - TcpTestViewModel: Socket 公共服务端测试 - SocketClientViewModel: TCP/UDP/TCP SSL 客户端 - PlotViewModel: 实时数据绘图面板 - 对应每个 ViewModel 创建 Avalonia UserControl 视图 - 使用 MVVM 模式,通过绑定和数据模板连接 - 显示版本、平台、运行时信息 - 链接到 GitHub、Issue、API 文档 - 新增 llcom.Avalonia/languages/zh-CN.xaml(60+ 键值) - 新增 llcom.Avalonia/languages/en-US.xaml(60+ 键值) - 通过 Avalonia ResourceDictionary 实现语言切换 - 使用 TabControl 承载 8 个选项卡 - 串口主界面 + Socket 服务端/客户端 + MQTT + 绘图 + 工具 + Lua + 关于 - 状态栏显示收发计数和平台信息 - PlatformHelper 新增 OpenUrl/GetPlatformName 方法 - 修复 llcom.Avalonia 命名空间与 Avalonia 框架命名空间冲突 - 添加 MQTTnet 4.3.7 跨平台 MQTT 客户端支持 - dotnet build llcom-cross.sln: 0 警告 0 错误
本轮完成所有跨平台迁移剩余任务的实现,原有 Windows WPF 项目不受影响。 - 添加 MoonSharp 2.0.0(纯 C# Lua 5.2 解释器)作为跨平台 Lua 引擎 - 重构 llcom.Core/LuaEnv/ 为 MoonSharp 实现: - LuaApis.cs: C# API 桥接(PrintLog/Utf8ToHex/Send/InputBox/AddPoint) - LuaEnv.cs: Lua 虚拟机,内嵌完整 LuatOS sys 协程调度框架 - LuaRunEnv.cs: 用户脚本运行环境(通道分发/定时器/命令执行) - LuaLoader.cs: 脚本加载器(全局 API 注册/require 路径/发送转换脚本管道) - 完全兼容原有 Lua 脚本生态(sys.wait/taskInit/timerStart/subscribe 等) - 零变更影响原 WPF 项目的 XLua 集成 - LuaScriptView: 脚本选择/运行/停止/命令执行/日志输出 - OnlineScriptsView: GitHub 在线脚本浏览/详情/下载(GraphQL API) - WinUsbView: USB 设备枚举/端点选择/数据收发框架 - GlobalState 单例:配置加载/settings.json/Hex 转换/在线脚本获取 - PlatformHelper 新增 InputDialogCallback - LinePlotPoint 改为 public 以支持跨程序集事件 - 添加 LibUsbDotNet 2.2.29 支持跨平台 USB 设备访问 - dotnet build llcom-cross.sln: 0 警告 0 错误
…indow) MainWindow 恢复 WPF 原版布局: - 3列 Grid + GridSplitter 拖拽分割(左数据显示 / 右Tab面板) - 左侧:数据收发显示 + 发送数据区(打开串口/清空/更多设置/TextBox/发送按钮) - 右侧:TabControl(快捷发送/Lua/在线脚本/小工具嵌套TabControl/关于) - 底部 StatusBar:刷新串口/端口选择/波特率/状态/TX/RX计数 - 语言切换按钮(右上角) QuickSend 快捷发送区: - ListBox + 自定义模板(编号/内容/发送按钮/Hex复选框/脚本图标) - 添加/删除/清空/导入/导出按钮 - 通过 QuickSendItem 持有命令引用解决 DataTemplate 内跨层级绑定问题 语言资源补充:QuickSend/Setting/ToolTip 相关键值
**根因**:Consolas 是 Windows 专有字体,Linux 下回退到不兼容 CJK 的等宽字体,导致中文字符压缩重叠。 **App.axaml 全局字体**: - 添加 TextBlock/TextBox 全局 CJK 回退链: Microsoft YaHei,SimHei,WenQuanYi Micro Hei,Noto Sans CJK SC,sans-serif - 移除无效的 Avalonia.Fonts.Inter StyleInclude **等宽字体替换** (14 个文件): - Consolas → "Cascadia Code,Consolas,DejaVu Sans Mono,monospace" - 覆盖:MainWindow/QuickSend/TcpTest/SocketClient/Mqtt/Convert/ WinUsb/OnlineScripts/LuaScript/SettingWindow **LineHeight 防 CJK 重叠**: - MainWindow ListBox DataTemplate TextBlock 添加 LineHeight="18" - SkiaSharp 渲染 CJK 等宽字体需要明确行高 **列宽/高度适配**: - QuickSend 表头列宽 30/25/25 → 35/28/28 - QuickSend 内容列宽 20/25 → 25/28 + MinHeight=26 - EncodingFixPage TextBox Height=60 → MinHeight=60 - TcpTestPage 行定义 60,30 → Auto,Auto - MqttPageView TabControl 移除 Height=400 - PlotPageView StackPanel → WrapPanel 防按钮挤压 **TextWrapping 补充**: - SocketClientPage TextBlock TextTrimming → TextWrapping - AboutPage Version/Framework TextBlock 添加 Wrap - EncodingFixPage 标签添加 Wrap
**根因**: SwitchLanguage() 方法体完全为空 (仅注释),按钮点击无任何效果
**修复**:
- MainWindowViewModel: SwitchLanguage 实现中/英切换逻辑
- _currentLanguage 状态追踪,zh-CN<->en-US 交替切换
- 调用 PlatformHelper.LoadLanguageFile() 通知 UI 层
- StatusText 显示切换结果反馈
- App.axaml.cs: 实现 LoadLanguageFileCallback
- 动态替换 Window.Resources.MergedDictionaries 中的 ResourceInclude
- avares://llcom/languages/{language}.xaml 协议加载对应语言文件
- MainWindowViewModel 构造: 移除无用的 no-op callback(避免被覆盖)
**运行时机制**:
点击 🌐 → SwitchLanguage() → LoadLanguageFile("en-US")
→ callback 执行 → Resources.MergedDictionaries.Clear() + 加载新语言文件
→ 所有 DynamicResource 引用的 UI 文本自动刷新
- 使用 LibUsbDotNet v2.2.29 实现完整 USB 设备枚举、连接、端点读写
- 后台线程接收数据,线程安全发送缓冲队列
- 支持 Lua 脚本通过 send('winusb', data) 注入发送
- 端点地址文本框支持 hex 输入 (0x81/0x01),自动解析 ReadEndPointId/WriteEndPointId
- WinUsbView.axaml 硬编码字符串 → DynamicResource 国际化
- 新增 SerialMonitorPage(ViewModel + View),对标 WPF 原版串口监听功能 - 进程列表枚举 (Process.GetProcesses) + COM 端口选择 (SerialPort.GetPortNames) - 调用 NativeInterop.MonitorComm/UnMonitorComm 底层 API - 回调解析 Udata 结构体显示收发/断开状态,启停按钮 + 日志面板 - 平台不可用时显示提示信息 (Linux/macOS 串口监听暂不支持) - MainWindow.axaml 修正 SerialMonitor Tab 绑定 (原错误指向 WinUsbView)
- LocaleHelper +30 keys (WinUSB 14个/SerialMonitor 7个/QuickSend/OnlineScripts等) - zh-CN.xaml + en-US.xaml 各新增 25+ DynamicResource keys - AboutPageView/LuaScriptView/SettingWindow 硬编码→DynamicResource - OnlineScriptsViewModel/QuickSendViewModel 消息→LocaleHelper - 修复 zh-CN.xaml + en-US.xaml WinUsbInEndpoint/WinUsbOutEndpoint 重复 key 崩溃
- AboutPageView.axaml 完整重构:图标、描述段落、版本号、开发者信息、QQ群、致谢项目列表 - 新增 13 个 i18n key(AboutDiscriptionMain/More/Developers/QQGroup/Translator/GitHubText/GitHubButton/FeedBack/FeedBackButton/ThanksProjects/Version + TcpServerAddr) - AboutViewModel 新增 AssemblyVersion(反射获取)+ ThanksProjects(更新为跨平台技术栈) - 修复 AboutGitHubButton / AboutFeedBackButton / TcpServerAddr 三个 DynamicResource key 缺失导致按钮文字不显示 - SerialMonitorNotAvailable 提示文本更新为含 Linux 替代方案建议(strace/socat)
There was a problem hiding this comment.
Code Review
This pull request ports the LLCOM serial debugging tool to Avalonia (.NET 8) for cross-platform support, introducing various views, view models, and a refactored Rust-based serial monitor library. The review feedback highlights several critical issues and improvement opportunities: cross-thread UI updates in the main view model, an infinite loop hazard on TCP connection closure, and missing legacy encoding registration at startup. Additionally, recommendations are provided to optimize hex parsing, improve send buffer queue efficiency, fix unawaited tasks in the Lua runner, prevent potential out-of-bounds exceptions during PID parsing, and avoid file-locking issues when extracting the Windows hook DLL.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| private void AppendDataLine(DataLineItem line) | ||
| { | ||
| ReceivedLines.Add(line); | ||
| while (ReceivedLines.Count > _maxLines) | ||
| ReceivedLines.RemoveAt(0); | ||
| } |
There was a problem hiding this comment.
Modifying ReceivedLines (an ObservableCollection bound to the UI) directly from the background thread in OnDataReceived will cause cross-thread exceptions or UI instability in Avalonia. You must marshal these modifications to the UI thread using Dispatcher.UIThread.Post.
private void AppendDataLine(DataLineItem line)
{
global::Avalonia.Threading.Dispatcher.UIThread.Post(() =>
{
ReceivedLines.Add(line);
while (ReceivedLines.Count > _maxLines)
ReceivedLines.RemoveAt(0);
});
}| var count = await _tcpClient.GetStream().ReadAsync(buffer, ct); | ||
| if (count > 0) | ||
| { | ||
| var data = new byte[count]; | ||
| Array.Copy(buffer, data, count); | ||
| global::Avalonia.Threading.Dispatcher.UIThread.Post(() => | ||
| AppendReceive($"→ {Encoding.UTF8.GetString(data)}")); | ||
| } | ||
| } |
There was a problem hiding this comment.
When the remote host gracefully closes the TCP connection, ReadAsync returns 0. If you do not handle this case, the loop will spin infinitely, consuming 100% CPU. You should break the loop and set IsConnected = false when count == 0.
var count = await _tcpClient.GetStream().ReadAsync(buffer, ct);
if (count == 0)
{
IsConnected = false;
break;
}
if (count > 0)
{
var data = new byte[count];
Array.Copy(buffer, data, count);
global::Avalonia.Threading.Dispatcher.UIThread.Post(() =>
AppendReceive($"→ {Encoding.UTF8.GetString(data)}"));
}
}| public static void Main(string[] args) => BuildAvaloniaApp() | ||
| .StartWithClassicDesktopLifetime(args); |
There was a problem hiding this comment.
In .NET Core/.NET 8, legacy encodings (such as GBK, Shift_JIS, Big5, etc.) are not registered by default. To prevent silent failures or exceptions when the application attempts to resolve these encodings (e.g., in the "Garbled code fix" tool), you must register the code pages provider at application startup.
public static void Main(string[] args)
{
System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
}| private static byte[] HexToBytes(string hex) | ||
| { | ||
| hex = System.Text.RegularExpressions.Regex.Replace(hex, "[^0-9A-Fa-f]", ""); | ||
| if (hex.Length % 2 != 0) hex = hex[..^1]; | ||
| var bytes = new byte[hex.Length / 2]; | ||
| for (int i = 0; i < bytes.Length; i++) | ||
| bytes[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16); | ||
| return bytes; | ||
| } |
There was a problem hiding this comment.
Instead of manually parsing the hex string using a loop with Substring (which allocates a new string for every byte and is highly inefficient), you can use the built-in Convert.FromHexString method after sanitizing the string.
private static byte[] HexToBytes(string hex)
{
hex = System.Text.RegularExpressions.Regex.Replace(hex, "[^0-9A-Fa-f]", "");
if (hex.Length % 2 != 0) hex = hex[..^1];
return Convert.FromHexString(hex);
}| private UsbEndpointWriter? _writer; | ||
| private Thread? _readThread; | ||
| private volatile bool _needClose; | ||
| private readonly List<byte[]> _sendBuffer = new(); |
There was a problem hiding this comment.
Using a List<byte[]> for _sendBuffer and calling RemoveAt(0) is highly inefficient because removing the first element of a list requires shifting all subsequent elements, resulting in O(N) complexity. Using a Queue<byte[]> provides O(1) complexity for enqueue and dequeue operations.
private readonly Queue<byte[]> _sendBuffer = new();| try | ||
| { | ||
| LuaEnv.LuaRunEnv.New(SelectedScript); | ||
| Task.Delay(200).ContinueWith(_ => { }); |
There was a problem hiding this comment.
| var start = SelectedProcess.IndexOf('['); | ||
| if (start < 0 || !uint.TryParse( | ||
| SelectedProcess.Substring(start + 1, SelectedProcess.Length - start - 2), | ||
| out var pid)) | ||
| { | ||
| StatusText = LocaleHelper.Get("SerialMonitorInvalidPid"); | ||
| return; | ||
| } |
There was a problem hiding this comment.
The current substring parsing logic for extracting the PID can throw an ArgumentOutOfRangeException if the SelectedProcess string does not strictly conform to the expected format (e.g., if it contains [ but no closing ], or ends prematurely). Using AsSpan with LastIndexOf is safer, more robust, and avoids string allocations.
var start = SelectedProcess.IndexOf('[');
var end = SelectedProcess.LastIndexOf(']');
if (start < 0 || end <= start + 1 || !uint.TryParse(
SelectedProcess.AsSpan(start + 1, end - start - 1),
out var pid))
{
StatusText = LocaleHelper.Get("SerialMonitorInvalidPid");
return;
}| fn extract_hook_dll(path: &PathBuf) -> bool { | ||
| std::fs::write(path, HOOK_DLL_BYTES).is_ok() | ||
| } |
There was a problem hiding this comment.
Writing the embedded hook DLL to the temp directory on every connection attempt will fail with a sharing/locking violation if another instance of the application is running or if the DLL is still loaded/locked by a monitored process. You should check if the file already exists before attempting to write it.
| fn extract_hook_dll(path: &PathBuf) -> bool { | |
| std::fs::write(path, HOOK_DLL_BYTES).is_ok() | |
| } | |
| fn extract_hook_dll(path: &PathBuf) -> bool { | |
| if path.exists() { | |
| return true; | |
| } | |
| std::fs::write(path, HOOK_DLL_BYTES).is_ok() | |
| } |
1. MainWindowViewModel: OnDataReceived/Sent 使用 Dispatcher.UIThread.Post 避免跨线程修改 ObservableCollection 2. SocketClientViewModel: TcpReceiveLoop 中 ReadAsync 返回 0 时处理对端优雅关闭 3. Program.cs: 注册 CodePagesEncodingProvider 以支持 GBK/Shift_JIS 等遗留编码 4. MqttViewModel + SocketClient: HexToBytes 改用 Convert.FromHexString 替代手写循环 5. WinUsbViewModel: 发送缓冲区从 List<byte[]> 改为 Queue<byte[]> (O(1) 出队) 6. LuaScriptViewModel: ToggleRun 改为 async Task 并用 await Task.Delay(200) 替代未正确等待的 ContinueWith 7. SerialMonitorViewModel: PID 解析改用 AsSpan + LastIndexOf 避免 Substring 下标越界 8. serial_monitor_rs/windows.rs: extract_hook_dll 先检查文件存在+大小匹配,避免重复写入导致文件锁冲突
MainWindow 功能补全: - 添加 Ctrl+Return 快捷键发送数据 (KeyBinding) - 状态栏新增 数据位/停止位/校验位 选择器(对标 WPF 更多设置面板) - 波特率列表扩展为 27 项(含 110~3000000 全部值 + 自定义选项) - 自定义波特率处理逻辑 语言文件补全 (~90 个新增 key): - 设置面板: 分包超时/空白间隔/最大包长/日志截断/终端模式等 - 快捷发送: 列表切换/脚本图标提示/导入导出消息/按钮设置 - Lua编辑器: 文件名/新建/确认/取消/已存在/失败提示 - MQTT: 已订阅列表/未连接/清洗会话 - TCP/Socket: 服务器端口/协议/重连/回车换行 - 在线脚本: 加载中/下载标题/保存提示 - USB: 连接/断开/枚举/端点错误消息 - About: 自动更新/检查更新 - 串口监听: 刷新/无效PID/启动失败 - 通用: 删除确认/输入对话框/原始数据/日志警告等
功能补全: 1. 发送数据 Lua 管道: SendData 通过 sendScript.lua 处理后再发送 2. 接收脚本每项绑定: QuickSendItem.recvScriptPath/Para 实际切换全局接收脚本 3. 自定义波特率: 选中"自定义"时弹出 InputDialog 输入自定义波特率 4. 窗口位置保存/恢复: 启动还原位置大小,关闭时保存到 window_state.json 5. Lua 自动保存: 编辑器失焦、选文件切换、窗口失活关闭时自动保存 6. 外部文件修改检测: 窗口激活时检查 Lua 文件是否被外部修改并重载 7. Lua 日志缓冲: 独立线程缓冲 + 过长自动清空(>1000行) + 暂停/继续 8. RTS/DTR 控制: 状态栏添加 RTS/DTR CheckBox 实时控制串口信号 9. RawDataSent 事件: 处理 UartDataRawSent 显示原始发送数据 10. InputDialog/filePicker 回调: App.axaml.cs 注册完整 UI 回调 11. CRLF 末尾追加: SendData/SendItem 支持 extraEnter 自动追加\r\n 安全修复: - 路径穿越防护: GetSafeScriptPath() 确保脚本路径在合法目录内 - 编码一致性: SendData 使用 Settings.encoding 而非硬编码 UTF8 - 事件解绑: Cleanup 中正确解绑所有 UART 事件
功能迁移: - Terminal 模式: 数据展示区键盘输入直接发送到串口(Ctrl+A-Z 发送控制码) - SettingWindow: OpenSettings 命令连接到实际 SettingWindow - SettingWindowViewModel: SaveSettings 持久化到 settings.json,双向绑定 GlobalState - QuickSend: 接收脚本图标(📜)弹出 ComboBox 选择脚本、参数图标(🛠)弹出输入对话框 - 右键图标清除相应配置 安全修复: - 路径穿越防护: LuaScriptViewModel.SaveCurrentScript/ConfirmNewScript, QuickSendViewModel.SendItem(RecvScriptPath), OnlineScriptsViewModel.DownloadScript 添加 GetSafeScriptPath/SanitizeFileName 验证 - ByteConvert.Hex2Byte: 添加 1MB 输入大小限制防 DoS - QuickSendViewModel.ImportData: 添加 10MB 文件大小限制 + JSON MaxDepth=32 - ConvertPageViewModel/SocketClientViewModel/MqttViewModel: 私有 Hex2Byte 替换为 共享 ByteConvert.Hex2Byte(已含大小限制) - PlatformHelper.OpenUrl: 添加 URL 验证防命令注入,移除 cmd /c 拼接 - MqttViewModel: 添加 TLS 证书绕过安全性注释
|
那你慢慢跑 |
4bab2cd to
533fe1c
Compare
功能迁移: - Terminal 模式: 数据展示区键盘输入直接发送到串口(Ctrl+A-Z 发送控制码) - SettingWindow: OpenSettings 命令连接到实际 SettingWindow - SettingWindowViewModel: SaveSettings 持久化到 settings.json,双向绑定 GlobalState - QuickSend: 接收脚本图标(📜)弹出 ComboBox 选择脚本、参数图标(🛠)弹出输入对话框 - 右键图标清除相应配置 安全修复: - 路径穿越防护: LuaScriptViewModel.SaveCurrentScript/ConfirmNewScript, QuickSendViewModel.SendItem(RecvScriptPath), OnlineScriptsViewModel.DownloadScript 添加 GetSafeScriptPath/SanitizeFileName 验证 - ByteConvert.Hex2Byte: 添加 1MB 输入大小限制防 DoS - QuickSendViewModel.ImportData: 添加 10MB 文件大小限制 + JSON MaxDepth=32 - ConvertPageViewModel/SocketClientViewModel/MqttViewModel: 私有 Hex2Byte 替换为 共享 ByteConvert.Hex2Byte(已含大小限制) - PlatformHelper.OpenUrl: 添加 URL 验证防命令注入,移除 cmd /c 拼接 - MqttViewModel: 添加 TLS 证书绕过安全性注释




No description provided.