QianTools 2.9.0

dotnet add package QianTools --version 2.9.0
                    
NuGet\Install-Package QianTools -Version 2.9.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="QianTools" Version="2.9.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="QianTools" Version="2.9.0" />
                    
Directory.Packages.props
<PackageReference Include="QianTools" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add QianTools --version 2.9.0
                    
#r "nuget: QianTools, 2.9.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package QianTools@2.9.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=QianTools&version=2.9.0
                    
Install as a Cake Addin
#tool nuget:?package=QianTools&version=2.9.0
                    
Install as a Cake Tool

QianTools

.NET 工业自动化与系统管理工具库,支持 net6.0 / net8.0。

作者:钱军涛 | 公司:浪涛科技有限公司 | 许可证:MIT

安装

dotnet add package QianTools

项目结构

QianTools/
├── Core/                    # 基础设施(日志、配置)
│   ├── LogManager.cs        # 日志管理器(支持 ILogger 接口、级别过滤、文件滚动)
│   ├── ILogger.cs           # 日志接口
│   ├── LogLevel.cs          # 日志级别枚举
│   ├── ConfigManager.cs     # 配置管理器(JSON/XML,策略模式可扩展)
│   ├── IConfigParser.cs     # 配置解析器接口
│   ├── JsonParser.cs        # JSON 解析器
│   └── XmlParser.cs         # XML 解析器
├── Helpers/                 # 通用工具类
│   ├── AllTools.cs          # 统一外观入口(FileTools / CryptoTools / NetworkTools)
│   ├── CryptoHelper.cs      # 加密与哈希(SHA256、MD5)
│   ├── FileHelper.cs        # 文件/目录复制与删除
│   └── NetworkHelper.cs     # HTTP 文件下载
├── System/                  # 系统硬件信息
│   ├── SystemHardwareInfo.cs    # 串口、磁盘、CPU、操作系统、网络适配器
│   └── MachineCodeGenerator.cs # 机器码生成与授权验证
├── Plc/                     # PLC 通信
│   ├── MelsecPlcService.cs  # 三菱 FX 底层通信服务(ASCII 协议)
│   └── MelsecPlcHelper.cs   # 上层静态封装
├── EtherNetIP/              # EtherNet/IP 与 Modbus TCP 通信
│   ├── EtherNetIPClient.cs  # EtherNet/IP 显式消息客户端
│   ├── Encapsulation.cs     # 封装协议头操作
│   ├── CipMessage.cs        # CIP 协议消息构建与解析
│   ├── EdsAssemblyReader.cs # Assembly 数据读取器
│   └── Ex1121ModbusClient.cs# EX-1121 Modbus TCP IO 客户端
├── Ezd/                     # JCZ激光打标板卡控制
│   ├── EzdClient.cs         # 高层封装(单例),生命周期/异步/业务级 API
│   ├── EzdNative.cs         # P/Invoke 底层接口声明
│   └── EzdErrorCode.cs      # 故障码定义与中文消息
├── Application/             # 应用层服务
│   ├── AutoStartManager.cs  # 注册表开机自启动
│   ├── ShortcutCreator.cs   # 桌面快捷方式
│   ├── VerifyInstallation.cs# 软件安装与验证
│   ├── MesClient.cs         # MES HTTP 客户端
│   └── MesTcpClient.cs     # MES TCP 通信客户端(JSON 换行协议)
├── QianTools.csproj
└── README.md

功能模块

Core — 基础设施

日志管理 — LogManager
// 使用静态方法(默认单例)
LogManager.Info("设备启动成功");
LogManager.Error($"通信失败: {ex.Message}");

// 自定义日志路径和文件滚动
LogManager.Initialize(@"D:\AppLogs\myapp.log");
LogManager.MinLogLevel = LogLevel.Debug;
LogManager.MaxFileSizeBytes = 10 * 1024 * 1024;  // 10MB 自动滚动

// 通过 ILogger 接口注入(支持单元测试)
ILogger logger = LogManager.Instance;
logger.LogInfo("消息");
配置管理 — ConfigManager

支持 JSON(默认)和 XML 两种格式,策略模式可扩展。

// ===== JSON 模式(默认) =====
var cfg = new ConfigManager();                          // 或 ConfigManager.Default
cfg.Set("AppSettings", new AppSettings { Theme = "Dark" });
var settings = cfg.Get<AppSettings>("AppSettings");     // 从内存读取
cfg.Save();                                              // 持久化到文件
cfg.Load();                                              // 从文件加载到内存
cfg.Remove("AppSettings");

// ===== XML 模式 =====
var xmlCfg = new ConfigManager(useXml: true, customPath: @"D:\MyApp\Config");
xmlCfg.Set("AppSettings", new AppSettings { Theme = "Dark" });
xmlCfg.Save();  // → D:\MyApp\Config\config.xml

// ===== 错误监听 =====
ConfigManager.OnError += msg => Console.WriteLine(msg);

Helpers — 通用工具

统一入口 — AllTools
// 通过 AllTools 静态子类访问
AllTools.FileTools.CopyDirectory(@"C:\Source", @"D:\Backup", overwrite: true);
AllTools.FileTools.DeleteDirectory(@"C:\Temp\OldData");
string sha256 = AllTools.CryptoTools.ComputeSha256Hash("hello");
string md5 = AllTools.CryptoTools.ComputeMd5Hash("hello");
await AllTools.NetworkTools.DownloadFileAsync(url, destinationPath);

// Excel 操作
AllTools.ExcelTools.WriteToExcel(dataList, @"D:\data.xlsx", "Sheet1");
DataTable dt = AllTools.ExcelTools.ReadToDataTable(@"D:\data.xlsx");
List<MyClass> list = AllTools.ExcelTools.ReadToList<MyClass>(@"D:\data.xlsx");
文件操作 / 加密 / 网络 / Excel
// 文件
FileHelper.CopyDirectory(@"C:\Source", @"D:\Backup", overwrite: true);
FileHelper.DeleteDirectory(@"C:\Temp\OldData");

// 加密
string sha256 = CryptoHelper.ComputeSha256Hash("hello");
string md5 = CryptoHelper.ComputeMd5Hash("hello");

// 网络
await NetworkHelper.DownloadFileAsync("https://example.com/file.zip", @"D:\Download\file.zip");

// Excel(基于 ClosedXML,仅 .xlsx)
ExcelHelper.WriteToExcel(dataList, @"D:\data.xlsx", "Sheet1");
ExcelHelper.WriteDataTable(dataTable, @"D:\data.xlsx", "Sheet1");
DataTable dt = ExcelHelper.ReadToDataTable(@"D:\data.xlsx");
List<MyClass> list = ExcelHelper.ReadToList<MyClass>(@"D:\data.xlsx");
string[] sheets = ExcelHelper.GetSheetNames(@"D:\data.xlsx");

System — 系统信息

硬件信息 — SystemHardwareInfo
string ports = SystemHardwareInfo.GetPort();          // 串口列表
string disks = SystemHardwareInfo.GetDiskInfo();       // 磁盘信息
string cpu = SystemHardwareInfo.GetCpuInfo();          // CPU 信息
string os = SystemHardwareInfo.GetOsInfo();            // 操作系统
string network = SystemHardwareInfo.GetNetworkAdapterInfo(); // 网络适配器
机器码与授权 — MachineCodeGenerator
// 生成定长机器码(CPU + 硬盘 + 网卡,SHA256 定长哈希)
string machineCode = MachineCodeGenerator.GenerateMachineCode();
string hash = MachineCodeGenerator.GetMachineCodeHash();  // SHA256 大写

// ── 授权方生成注册码(口令必填;QT2 新格式,HMAC-SHA256 签名)──
string licenseKey = MachineCodeGenerator.GenerateLicenseKey(
    machineCode, new DateTime(2026, 12, 31, 0, 0, 0, DateTimeKind.Utc),
    password: "admin@123");

// ── 用户端验证(返回 LicenseResult)──
LicenseResult result = MachineCodeGenerator.ValidateLicenseKey(
    licenseKey, machineCode, password: "admin@123");
// result.IsValid / result.ExpiryDate / result.ErrorReason
// result.MachineCodeOk / result.DateOk / result.PasswordOk / result.HasPassword

// ── 生产环境必须注入自有签名密钥(默认密钥仅测试用,随客户端分发)──
MachineCodeGenerator.SetLicenseSigningKeyOnce("your-own-strong-key");

Plc — PLC 通信

MelsecPlcHelper

支持三菱 FX 系列(X/Y/M/S/T/C/D 元件),ASCII 协议串口通信。

// 连接 PLC
MelsecPlcHelper.Connect("COM1", baudRate: 9600);

// 读取
bool x0 = MelsecPlcHelper.ReadX(0);
bool y1 = MelsecPlcHelper.ReadY(1);
bool m10 = MelsecPlcHelper.ReadM(10);
short d100 = MelsecPlcHelper.ReadD(100);
short t5 = MelsecPlcHelper.ReadT(5);

// 写入
MelsecPlcHelper.WriteY(1, true);
MelsecPlcHelper.WriteM(10, false);
MelsecPlcHelper.WriteD(100, 12345);

// 轮询 D 寄存器
MelsecPlcHelper.OnDataUpdated += snapshot => {
    Console.WriteLine($"D100 = {snapshot.D[100]}");
};
MelsecPlcHelper.StartPolling(new[] { 100, 101, 102 }, intervalMs: 100);

// 日志监听
MelsecPlcHelper.OnLog += (msg, level) => Console.WriteLine($"[{level}] {msg}");

// 断开
MelsecPlcHelper.Shutdown();

Plc 协议库 — 三菱 MC / Modbus RTU-ASCII / 西门子 S7(v2.9 新增)

命名空间: QianTools.Plc / QianTools.Plc.Melsec / QianTools.Plc.Modbus / QianTools.Plc.Siemens

依据各协议官方手册从零实现(MIT 洁净室),与旧版 MelsecPlcHelper(FX 串口)/ Ex1121ModbusClient(Modbus TCP)并存、互不影响。所有客户端共用一套基座约定:

  • 事件:OnConnectionChanged(连接状态)/ OnLog(日志,对接 LogManager)
  • 超时:请求-响应真实超时(TimeoutException,超时后连接失效、下次操作自动重连)
  • 释放:DisposeAsync()/Dispose() 会取消并排空在途请求(安全幂等,回调内可调用)
  • 无实机验证:每个协议都带进程内模拟器(*Simulator),配合帧级单元测试可实现无硬件闭环联调;生产使用前建议按 移植方案.md 的实机清单抓帧复核
三菱 MC(以太网,A/E/Q/L 系列)— MelsecMcClient
using QianTools.Plc.Melsec;

var plc = new MelsecMcClient("192.168.1.50");   // 默认端口 5000、二进制 3E 帧
await plc.ConnectAsync();

// 位软元件读写(X/Y/M/L/B/S/SM 等)
bool[] m = await plc.ReadBoolAsync("M", 0, 16);
await plc.WriteBoolAsync("M", 10, new[] { true, false, true });

// 字软元件读写(D/W/R/ZR/SD/Z 等,低 16 位在前)
short[] d = await plc.ReadInt16Async("D", 100, 4);
await plc.WriteInt16Async("D", 200, new short[] { 123, -456 });

// 32 位 / 浮点(相邻 2 寄存器)
int[] d32 = await plc.ReadInt32Async("D", 300, 2);
await plc.WriteFloatAsync("D", 310, new[] { 3.14f });

// 跨地址随机读(一次请求多组地址)
ushort[] rand = await plc.ReadRandomWordsAsync(
    new[] { ("D", 100), ("D", 200), ("W", 0x10) });

// ASCII 帧模式(对应模块需配置为 ASCII)
var asciiPlc = new MelsecMcClient("192.168.1.50", 5000, McFrameFormat.Ascii);
await asciiPlc.ConnectAsync();

// 帧参数(多网络/多 CPU 场合)
plc.NetworkNumber = 0;      // 网络号
plc.PLCNumber = 0xFF;       // PLC 号
plc.StationNumber = 0;      // 站号
plc.IOTarget = 0x03FF;      // 请求目标 IO 号
plc.TimerValue = 0x0010;    // 监视定时器

await plc.DisposeAsync();
  • 批读自动分包(二进制 950 字/7168 位、ASCII 460 字/3584 位),无需关心单帧上限
  • 设备错误码抛 McCommunicationException(含中文错误说明)
  • 位软元件按字读写采用 8 字节/字打包(与手册一致,实机复核项)
三菱 FX 串口升级(现有 MelsecPlcHelper 的新增 API)
// 系列型号校验(只收紧新增接口,老接口行为不变)
MelsecPlcHelper.FxSeries = FxSeries.FX3U;

// D 寄存器批量写(1~49 个,禁止跨 D7999/D8000 边界)
MelsecPlcHelper.WriteDBatch(100, new short[] { 1, 2, 3, 4, 5 });

// 位软元件批量读写(X/Y/M/S)
bool[] bits = MelsecPlcHelper.ReadBitBatch('M', 0, 16);
MelsecPlcHelper.WriteBitBatch('Y', 0, new[] { true, false, true });

// T/C 触点(线圈)访问(与现有 ReadT/ReadC=当前值语义区分)
bool tc = MelsecPlcHelper.ReadTContact(5);
MelsecPlcHelper.WriteCContact(8, true);
无实机闭环测试 — MelsecMcSimulator
using var sim = new MelsecMcSimulator(McFrameFormat.Binary);
sim.Start();                                     // 端口自动分配
sim.SetWord("D", 100, 0x1234);                   // 预置寄存器
sim.SetBits("M", 0, new[] { true, false, true });

using var plc = new MelsecMcClient("127.0.0.1", sim.Port);
await plc.ConnectAsync();
short v = (await plc.ReadInt16Async("D", 100, 1))[0];   // 0x1234
Modbus RTU / ASCII(串口)— ModbusRtuClient / ModbusAsciiClient
using QianTools.Plc.Modbus;

// 串口构造(从站地址、校验位、停止位可配)
using var rtu = new ModbusRtuClient("COM3", baudRate: 9600,
    dataBits: 8, parity: System.IO.Ports.Parity.None,
    stopBits: System.IO.Ports.StopBits.One, slaveAddress: 1);

// 功能码 01/02/03/04(读线圈/离散输入/保持寄存器/输入寄存器)
bool[] coils = await rtu.ReadCoilsAsync(0, 8);
ushort[] regs = await rtu.ReadHoldingRegistersAsync(0, 10);

// 功能码 05/06/0F/10(单/批量写)
await rtu.WriteSingleCoilAsync(10, true);
await rtu.WriteSingleRegisterAsync(100, 0xABCD);
await rtu.WriteMultipleCoilsAsync(20, new[] { true, false, true });
await rtu.WriteMultipleRegistersAsync(200, new ushort[] { 1, 2, 3 });

// ASCII 模式(帧格式 ":...CRLF",其余 API 相同)
using var ascii = new ModbusAsciiClient("COM3", 9600, 8,
    System.IO.Ports.Parity.Even, System.IO.Ports.StopBits.One, slaveAddress: 1);
ushort[] v = await ascii.ReadHoldingRegistersAsync(0, 2);
  • 支持注入自定义 IModbusByteTransport(网关/测试环回),超时由客户端统一管控
  • 从站返回异常时抛 ModbusException(含中文异常码说明,01/02/03/04)
  • 上限:读 2000 线圈 / 125 寄存器,写 1968 线圈 / 123 寄存器(Modbus PDU≤253 字节)
西门子 S7(ISO-on-TCP,S7-300/400/1200/1500)— SiemensS7Client
using QianTools.Plc.Siemens;

// 默认端口 102;连接时自动完成 ISO-on-TCP 握手 + PDU 协商(建议 480)
using var s7 = new SiemensS7Client("192.168.0.1", pduSize: 480);
await s7.ConnectAsync();
Console.WriteLine(s7.NegotiatedPduSize);     // 与 PLC 协商后的实际 PDU

// 字节读写(DB 区 / M 区 / I / Q)
await s7.WriteBytesAsync(S7Area.DataBlock, 1, 0, new byte[] { 0x11, 0x22 });
byte[] data = await s7.ReadBytesAsync(S7Area.DataBlock, 1, 0, 4);
byte[] mb = await s7.ReadBytesAsync(S7Area.Merker, 0, 10, 2);

// 位读写(读-改-写,客户端锁内原子)
bool b = await s7.ReadBitAsync(S7Area.DataBlock, 1, 0, 3);   // DB1.DBX0.3
await s7.WriteBitAsync(S7Area.Merker, 0, 5, 1, true);        // M5.1

// 类型化读写(大端)
await s7.WriteInt32Async(S7Area.DataBlock, 1, 8, new[] { 0x12345678 });
int[] dword = await s7.ReadInt32Async(S7Area.DataBlock, 1, 8, 1);
await s7.WriteFloatAsync(S7Area.DataBlock, 1, 20, new[] { 3.14f });

// 多区域批读(一次请求,自动按 PDU 分组)
var batch = await s7.ReadBytesBatchAsync(new[]
{
    new S7ReadRequest(S7Area.DataBlock, 1, 0, 16),
    new S7ReadRequest(S7Area.Merker, 0, 0, 4),
    new S7ReadRequest(S7Area.Input, 0, 0, 2),
});

// 地址字符串解析("DB1.DBX0.0" / "M5.3" / "MB10" / "IB0" / "Q1.7")
var (area, db, byteAddr, bit) = S7Address.Parse("DB1.DBX4.2");

// TSAP(机架/槽)与 PDU 配置
s7.LocalTSAP = 0x0100;
s7.RemoteTSAP = 0x0100;      // 部分设备为 0x0200/0x0300(见移植方案.md 实机清单)
  • 断线自动重连并重新握手(透明恢复)
  • 设备错误码抛 S7Exception(0x0005 地址越界 / 0x000A DB 不存在 / 0x8104 上下文不支持等,中文说明)
无实机闭环测试 — S7Simulator
using var sim = new S7Simulator();
sim.Start();
sim.CreateDb(1);
sim.SetBytes(S7Area.DataBlock, 1, 0, new byte[] { 0xAA, 0xBB });

using var s7 = new SiemensS7Client("127.0.0.1", sim.Port, pduSize: 480);
await s7.ConnectAsync();
byte[] v = await s7.ReadBytesAsync(S7Area.DataBlock, 1, 0, 2);   // AA BB

实机验证提醒:协议实现已通过"帧级单测 + 内嵌模拟器闭环 + 独立实现交叉对拍"三重验证,但部分细节(三菱 T/C 触点基址与 E00 扩展帧、S7 偶数对齐填充与 TSAP 机架配置)仍需实机抓帧确认,清单见 移植方案.md。

EtherNetIP — EtherNet/IP 与 Modbus TCP 通信

命名空间: QianTools.EtherNetIP

支持 EX-1121(DECOWELL/Keyence 兼容)远程 IO 模块,提供 EtherNet/IP 显式消息和 Modbus TCP 两种通信方式。

EtherNet/IP 客户端
using QianTools.EtherNetIP;

var client = new EtherNetIPClient("192.168.1.100", timeout: TimeSpan.FromSeconds(3));
await client.ConnectAsync();

// 读取输入 Assembly(Instance 100)— 过程镜像输入数据
byte[] inputData = await client.ReadInputAssemblyAsync();
var reader = new EdsAssemblyReader(inputData);

// 按偏移量读取数据
ushort ch0 = reader.ReadUInt16(0);        // 16 位无符号
short ch1 = reader.ReadInt16(2);          // 16 位有符号
uint ch2 = reader.ReadUInt32(4);          // 32 位无符号
bool bit0 = reader.ReadBool(0, 0);        // 位读取

// 读取输出 Assembly(Instance 101)
byte[] outputData = await client.ReadOutputAssemblyAsync();

// 显式消息:GetAttributeSingle
byte[] data = await client.GetAttributeAsync(
    classId: 0x04, instanceId: 100, attributeId: 0x03);

// 显式消息:SetAttributeSingle
await client.SetAttributeAsync(0x04, 101, 0x03, new byte[] { 0x00, 0x01 });

// Keyence / Decowell EDS 参数读取(服务 0x4C)
ushort param = await client.ReadParameterUInt16Async(slot: 1, paramNo: 512);
short param2 = await client.ReadParameterInt16Async(slot: 1, paramNo: 513);
uint param3 = await client.ReadParameterUInt32Async(slot: 1, paramNo: 514);

client.Disconnect();
Modbus TCP 客户端
var config = Ex1121Config.Default;  // DI=0xA000, DO=0xB000, 可自定义
using var io = new Ex1121ModbusClient("169.254.193.30", config, port: 20001);
await io.ConnectAsync();

// 读 DI
ushort[] inputs = await io.ReadInputsAsync();
bool di15 = Ex1121ModbusClient.GetDiBit(inputs, 15);

// 读 DO
ushort[] outputs = await io.ReadOutputsAsync();
bool do0 = Ex1121ModbusClient.GetDoBit(outputs, 0);

// 写 DO
await io.WriteOutputsAsync(0x0001);              // DO0 = ON
await io.SetDoBitAsync(3, true);                 // DO3 = ON

// 自定义寄存器地址读写
ushort[] regs = await io.ReadRegistersAsync(0xA000, 2);
await io.WriteRegistersAsync(0xB000, new ushort[] { 0x0001 });

io.Disconnect();

Application — 应用层服务

MES 通信 — MesClient
// GET 请求
string result = await MesClient.SendGetRequestAsync(
    "https://mes-server/api", "production/orders", apiKey: "your-token");

// POST 请求
var data = new { OrderId = "P001", Quantity = 100 };
string response = await MesClient.SendPostRequestAsync(
    "https://mes-server/api", "production/report", data, apiKey: "your-token");
MES TCP 通信 — MesTcpClient

基于 TCP + 换行分隔 JSON 协议,支持请求-响应模式和服务器主动推送。线程安全,适合与产线 MES 服务器进行长连接双向通信。

using QianTools.Mes;

var mes = MesTcpClient.Instance;

// ── 超时设置(连接前或连接后均可设置)──
mes.TimeoutMs = 10000;                                    // 请求-响应超时 10 秒

// ── 连接 ──
bool ok = await mes.ConnectAsync("192.168.1.100", 8080);  // 使用 TimeoutMs 属性值
// 也可在连接时显式传入超时
bool ok2 = await mes.ConnectAsync("192.168.1.100", 8080, timeoutMs: 3000);
// 连接状态通过事件通知
mes.OnConnectionChanged += connected => Console.WriteLine(connected ? "已连接" : "已断开");
mes.OnMessageReceived += json => Console.WriteLine($"服务器推送: {json}");
mes.OnError += err => Console.WriteLine($"异常: {err}");

// ── 测试连接(发送 ping)──
var (pingOk, pingResp) = await mes.TestAsync();

// ── 上报打标结果 ──
var (resultOk, resultResp) = await mes.SendResultAsync(
    productCode: "PC001",
    workOrder: "WO-20260523",
    productionLine: "L1",
    success: true,
    entityName: "Text1",
    fileName: @"C:\Files\product.ezd");

// ── 通用请求-响应 ──
string? response = await mes.SendAndReceiveAsync("{\"cmd\":\"query_status\"}");

// ── 单向发送(不等待响应)──
await mes.SendAsync("{\"cmd\":\"heartbeat\"}");

// ── 断开 ──
mes.Disconnect();

协议格式:每条 JSON 消息以 \n 结尾。常见消息类型:

方向 cmd 说明
客户端→服务端 ping 心跳/连通性测试
客户端→服务端 mark_result 上报打标结果(含产品代码、工单号、成功/失败等)
服务端→客户端 start_mark 要求开始标刻
服务端→客户端 select_entity 切换打标对象
服务端→客户端 update_product 更新生产信息(产品代码/工单号等)

线程安全:所有 I/O 通过 SemaphoreSlim 串行化。后台读循环自动处理服务器主动推送的消息。

桌面快捷方式 — ShortcutCreator
ShortcutCreator.CreateShortcut("我的应用", @"D:\App\MyApp.exe", arguments: "-start");
开机启动 — AutoStartManager
AutoStartManager.EnsureAutoStart("MyApp", @"D:\App\MyApp.exe", enable: true);
AutoStartManager.EnsureAutoStart("MyApp", null, enable: false);  // 取消
安装验证 — VerifyInstallation
// 检查软件是否安装
bool installed = VerifyInstallation.IsSoftwareInstalled("MyApp", version: "2.0");

// 安装程序(需用户确认委托)
VerifyInstallation.Notify = (title, msg) => MessageBox.Show(msg, title) == DialogResult.OK;
await VerifyInstallation.InstallAsync("安装 MyApp", @"D:\Installer", "setup.exe");

Ezd — JCZ 激光打标板卡控制

命名空间: QianTools.Ezd
平台要求: MarkEzd.dll 为 32 位原生库,宿主程序必须编译为 x86(<PlatformTarget>x86</PlatformTarget>)。x64 进程调用会直接返回 EzdErrorCode.ERR_PLATFORM_X86_REQUIRED (2001),不会崩溃。
运行依赖: 初始化时传入的 EZCAD 目录需包含 MarkEzd.dll、EZCAD.CFG、plug/、PARAM/、FONT/ 等 EZCAD 运行时文件;运行前必须关闭 ezcad2.exe(否则初始化返回 1“EZCAD 正在运行”)。

通过 P/Invoke 封装 MarkEzd.dll(对照官方《EZCAD2 开发者手册》v3.1,__cdecl + Unicode),提供初始化/关闭、文件加载保存、实体管理、标刻/红光、笔号/填充/字体/条码/位图参数、IO/锁存、振镜/扩展轴、连续飞行缓冲区等功能。

using QianTools.Ezd;

var ezd = EzdClient.Instance;

// ── 初始化(失败不会把状态卡在 Initializing;重复初始化会被拒绝)──
int ret = ezd.Initialize2(@"C:\Laser\EzCad2", testMode: false);
if (!EzdErrorCode.IsSuccess(ret))
{
    Console.WriteLine($"初始化失败: {EzdErrorCode.GetMessage(ret)}");
    return;
}

// ── 文件操作 ──
ezd.LoadEzdFile(@"C:\Files\product.ezd");
ezd.SaveEzdFile(@"C:\Files\output.ezd");

// ── 实体管理 ──
int count = ezd.GetEntityCount();
string firstName = ezd.GetEntityName(0);
// AddText(text, name, x, y, z, align, angle(弧度), penNo, hatch)
ezd.AddText("Hello", "Text1", 0, 0, 0, 1, 0, 1, false);
// AddCircle(cx, cy, radius, name, penNo) —— 手册无 hatch 参数
ezd.AddCircle(0, 0, 10, "Circle1", 1);
ezd.MoveEntity("Text1", 10, 5);
ezd.RotateEntity("Text1", 0, 0, Math.PI / 4);   // 角度单位:弧度
ezd.SetText("Text1", "NewText");                // 修改文本内容
ezd.DeleteEntity("Text1");

// ── 标刻控制(异步:阻塞的原生函数已用 Task.Run 包装)──
int r1 = await ezd.MarkAsync(flyMark: false);
int r2 = await ezd.MarkEntityAsync("Text1");
int r3 = await ezd.MarkLineAsync(0, 0, 50, 50, 1);
int r4 = await ezd.MarkPointAsync(100, 100, 1, 1);
ezd.StopMark();

// ── 红光预览 ──
await ezd.RedLightMarkAsync();
await ezd.RedLightContourAsync();
await ezd.RedLightEntityAsync("Text1", contour: true);
// 连续红光:在调用线程内循环,需自行 Task.Run 且提供停止条件
Task.Run(() => ezd.RedLightContourLoop(() => shouldStop));

// ── 预览图(HBITMAP 必须手动释放;Cbitmap* 版本不要用 DeleteObject)──
IntPtr hBmp = ezd.GetPreviewBitmap(320, 240);
IntPtr hBmp2 = ezd.GetEntityPreviewBitmap("Text1", 320, 240);
// 需要自定义视口时:GetPreviewBitmap3(width, height, originX, originY, scale)
if (hBmp != IntPtr.Zero) EzdNative.DeleteObject(hBmp);
if (hBmp2 != IntPtr.Zero) EzdNative.DeleteObject(hBmp2);

// ── 组合 / 复制 / 填充(source=填充参数来源对象,target=被填充对象)──
ezd.GroupEntities("A", "B", "AB", 1);
ezd.UnGroupEntity("AB");
ezd.CopyEntity("A", "A_copy");
ezd.SetDefaultHatchParam(enableContour: true, paramIndex: 1, enableHatch: true,
    penNo: 1, hatchType: 0, allCalc: false, edge: false, averageLine: false,
    Math.PI / 4, 0.05, 0, 0, 0, 0, 0, 1, false, false, 0);
ezd.HatchEntity("Source", "Target");

// ── 笔号参数(完整 32 参数版本)──
ezd.SetPenParam4(1, "MyPen", 0xFF0000, false, false, 1, 1000, 50, 10,
    20000, 1, 0, 0, 0, 0, 5000, 0, 0, 0, 0, false, 0, 0, false, 0,
    false, 0, 0, 0, false, 0, 0);

// ── IO / 锁存 / 振镜 / 扩展轴 ──
ezd.WritePort(0x00FF);
ezd.ReadPort(out ushort inData);
ezd.LaserOn(false);
ezd.EnableLock(true);                  // 参数 bLowToHigh:true=启用锁存
ezd.ClearLock();
ezd.GotoPos(100, 100);
ezd.GetCurCoor(out double gx, out double gy);
ezd.InitAxis(true, 0.5, 1, 50, false, 0, 0, 0);   // 先 Reset 使能,再写入轴参数
ezd.AxisMoveTo(0, 120);

// ── 条码(barWidthScale / spaceWidthScale 对应手册 double[4],长度必须 ≥4)──
ezd.AddBarcode("123456", "BC1", 0, 0, 0, 1, 1, false, 0, 0, 10, 0.3,
    new double[4], new double[4], 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, "Arial");

// ── 位图参数(按手册 8 参数版本,返回位图路径)──
ezd.GetBitmapParam("BC1", out string bmpPath, out int bmpAttrib, out int scanAttrib,
    out double brightness, out double contrast, out double pointTime, out int dpi);
// 完整版:GetBitmapParam2 / SetBitmapParam2(含低灰度值)
// DPI + 256 级灰度映射:GetBitmapParam3 / SetBitmapParam3

// ── 连续飞行缓冲区(SetTextName 最多 6 个;FlyAdd = count + 最多 6 个数据)──
ezd.ContinueBufferSetTextName("T1", "T2");
ezd.ContinueBufferFlyAdd(2, "A", "B");
int flyStart = await ezd.ContinueBufferFlyStartAsync();

// ── 错误处理 / 事件 ──
ezd.OnError += msg => Console.WriteLine($"EZCAD 错误: {msg}");
if (!EzdErrorCode.IsSuccess(flyStart))
    Console.WriteLine(EzdErrorCode.GetMessage(flyStart));

// ── 退出:关闭板卡并卸载 MarkEzd.dll ──
ezd.Shutdown();

说明:2.7.1 起按官方手册修正了 lmc1_GetPrevBitmap3、4 个位图参数函数、lmc1_AddBarCodeToLib、lmc1_AddCircleTextToLib、连续飞行、EnableLockInputPort、笔号基础版等声明,并修正了生命周期、x86 平台检查与文档示例。旧版错误签名导致的调用会崩溃,升级后请按上方示例调整调用。

2.8.0 行为变更(升级注意)

  • MesTcpClient:请求超时/取消后连接会被重置,需重新 ConnectAsync(LastError 会给出提示);响应按 \n 组帧,支持任意长度(不再有 4 KB 上限)。
  • ConfigManager:OnError 由 internal 改为 public;JSON/XML 反序列化默认 TypeNameHandling.None;枚举/Nullable/Guid/TimeSpan 等类型可正确往返;Save 原子写(临时文件 + File.Replace)。
  • LogManager:默认改为异步批量写,新增 Flush/FlushAsync/Shutdown/SynchronousMode;进程退出时自动 flush;同一日志文件的多进程追加使用命名 Mutex 串行化(同会话;跨会话共享文件需改用 Global\ + ACL)。
  • Excel/CSV:ExcelHelper.ReadToDataTable 列类型统一为 object、空文本读为 DBNull、按 FirstRowUsed/LastRowUsed 定位(前导空行不再错位);CsvHelper 数值/日期使用 InvariantCulture / ISO 8601,新增流式与编码重载、CsvParseError 错误收集。
  • NetworkHelper:两参下载方法保留,但不再带 100 秒总超时;请使用新增的 CancellationToken 重载,下载失败不会留下半文件。
  • Plc(MelsecPlcService):新增 ReadDBatch(轮询自动合并连续地址);轮询失败点保留上次有效值,用新增 FailedAddresses/IsComplete/TimestampUtc 判断质量;断线后自动退避重连;Dispose 立即释放 COM 口;E00 仅按 D≥8000 识别。
  • 注册码/机器码:机器码改为 SHA256(CPU+硬盘+MAC) 定长哈希(旧机器码仍可回退验证);注册码新格式 QT2(HMAC-SHA256 签名,旧码可验证;旧客户端无法验证新码,请先升级验证端再发新码);生产环境可通过 LicenseSigningKey 覆盖内置签名密钥。
  • EtherNetIP/Modbus:Modbus MBAP 头修正为标准布局(TxID/Protocol/Length/UnitId/Func),Ex1121Config 新增 UnitId(默认 1)与 TimeoutMs;收发改为一问一答串行化并校验 txId/func;EIP 异步收发有真实超时(TimeoutException),超时后连接失效需重连。UnitId/MBAP 修正需实机抓包复验。
  • RemoteIODevice:实现 IAsyncDisposable;同步 Dispose() 可在 IODataUpdated 回调内安全调用;即时输出刷新改为 dirty 循环,不再丢写;轮询连续失败自动退避重连。

质量与测试

  • 单元测试:tests/QianTools.Tests(xUnit)。本地运行:dotnet test tests/QianTools.Tests/QianTools.Tests.csproj -c Release
  • CI:GitHub Actions CI workflow 在 push/PR 时构建 net6.0-windows + net8.0-windows 并运行测试
  • 打包:dotnet pack -c Release 同时生成 QianTools.*.nupkg 与 QianTools.*.snupkg(SourceLink 已启用,仓库地址 https://github.com/juntaoqian/QianTools)

发布

dotnet pack -c Release
dotnet nuget push bin\Release\QianTools.*.nupkg --source https://api.nuget.org/v3/index.json
Product Compatible and additional computed target framework versions.
.NET net6.0-windows7.0 is compatible.  net7.0-windows was computed.  net8.0-windows was computed.  net8.0-windows7.0 is compatible.  net9.0-windows was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.9.0 0 10/25/2026
2.8.0 51 10/24/2026
2.7.1 50 10/23/2026
2.7.0 328 7/30/2026
2.6.5 127 7/26/2026
2.6.1 127 7/26/2026
2.6.0 127 7/21/2026
2.5.0 129 7/21/2026
2.4.0 123 7/21/2026
2.3.5 149 5/19/2026
2.3.4 119 5/19/2026
2.3.3 136 5/19/2026
2.3.2 133 5/16/2026
2.3.1 138 5/15/2026
2.3.0 138 5/15/2026
2.1.1 218 12/23/2025
1.1.3 211 12/23/2025
1.1.0 218 12/23/2025
1.0.0 203 7/20/2024

2.9.0:新增 QianTools.Plc 工业通信协议库(MIT 洁净室实现,依据官方协议手册,可无实机闭环测试)——① 三菱 MC 客户端 MelsecMcClient(MC 二进制/ASCII 3E 帧,A/E/Q/L 系列,位/字/32 位/浮点读写、跨地址随机读、批读自动分包、中文错误码)与内嵌模拟器 MelsecMcSimulator;② 三菱 FX 串口升级(WriteDBatch 批写、位软元件批读写、T/C 触点访问、FxSeries 系列地址校验,老 API 冻结);③ Modbus RTU/ASCII(ModbusRtuClient/ModbusAsciiClient,功能码 01/02/03/04/05/06/0F/10、CRC16/LRC、IModbusByteTransport 传输抽象);④ 西门子 S7(SiemensS7Client:ISO-on-TCP 握手、PDU 协商、DB/M/I/Q 字节与位读写、类型化大端读写、多区批读、S7Address 地址解析)与内嵌模拟器 S7Simulator;⑤ 公共基座 PlcDeviceBase/PlcSerialDeviceBase(真实超时、断线自动重连、dispose 排空)与 ByteTransform/PlcResult。全部协议经"帧级单测 + 模拟器闭环 + 独立实现交叉对拍"三重验证;同时完成 2.8.0 的评审修复(依赖漏洞清零、Dispose 竞态统一模板、授权时区/时钟回拨修复等,详见仓库 REVIEW.md)。