激光雕刻切割控制系统 V7.92.2 网络通信添加状态显示
前几天搬工作室,电脑什么的都搬到百米远的地方,激光切割机因为体积大而且笨重,更重要的的是排烟的新工作室不好安装,所幸就暂时留在那了。最开始得到时候切割东西都是这边做好微信传输过去,拿笔记本在连接上切割机的WIFI,进行切割的,一整二整反复修改切割,除了百米“赛跑”外还要不停的切换WIFI,整的火气比较大。后来优化了一下,把切割机的路由器做了一下设置,桥接到主路由无线上(如果不重新设置切割机的IP,就使用子网的方式,映射IP端口到外部;设置的话可以选择把路由器当交换机使用),这样调整后的好处是不需要拷贝到另外一台电脑再操作,直接传输即可。但是又想要的新东西,在切割机工作的时候,软件(LaserCAD)并不会显示当前的是否处于“运行”还是“空闲”状态,虽然它在运行中的时候加载文件,会提示当前正在运行,并不是直观。所以决定在软件的控制面板的底下添加一个用来显示当前状态的文本作为提示,然后也就有了这篇文章。
1. 获取激光切割机的状态
2. 在软件中添加用于显示状态的Static
3. 使用程序控制状态显示
获取激光切割机的状态
获取状态,就需要搞清楚软件和激光切割机是如何通信的,获取状态所发送的消息内容是什么,这些都是封装在程序里面的,并且这个软件并不开源,从源代码获取是不可能了,而且文档上也没有说到传输协议这个信息,还好知道它是有用网络通信的,这就是我们的突破口。
准备软件:
Wireshark (用于网络抓包) 选择要抓包的网卡,然后设置过滤 ip.addr == 192.168.102.15
(后面的IP地址是 )
点击获取当前坐标位置:
通过捉取的信息我们可以发现激光切割机监听的端口为 2027 ,之后我们也是需要把数据发送到这个端口上的,协议为UDP
在我还不知道切割机器在运行中加载文档时会提示:“传输失败:机器正在工作或暂停!”时,我才用的方法时获取两次当前的坐标位置,如果坐标位置一样就说明机器已经处于空闲状态,否则正在运行。有了这个思路,就用node.js 写了一个简易版本,测试是否可行。先看看获取当前坐标位置到做了什么事:
这个是发送的16进制数据,看Ascii码明显是加密过的可以使用右键复制值,或则…as HEX Dump
对其进行解密得到:
00 00 00 01 d7 00 04 21 d7 00 04 31 d7 00 04 41 d7 00 04 51
解密方法为对每一个字节(bit):~bit & 0xff ^ 0x82
比如开始:7d 7d 7d 7c a5 3e00 00 00 01 d8 43
暂停/继续:7d 7d 7d 7c a5 390 00 00 01 d8 44
停止:7d 7d 7d 7c a5 3800 00 00 01 d8 45
前面的 00 00 00 01 表示发送
后面的 d7 00 表示类型 也可以尝试转成十进制
再后面携带的数据 接收的数据:
解密后:00 00 00 02 d7 01 04 21 00 00 0b 22 6c d7 01 04 31 00 00 03 69 0f d7 01 04 41 00 00 18 35 00 d7 01 04 51 00 00 00 00 00
前面的 00 00 00 02 表示接收
后面是每个轴和对应的坐标
d7 01 04 21 00 00 0b 22 6c X坐标 = 184.06
d7 01 04 31 00 00 03 69 0f Y坐标 = 62.61
d7 01 04 41 00 00 18 35 00 Z坐标 = 400d7 01 04 51 00 00 00 00 00
#include <iostream>#include <iomanip>using namespace std; int decode(int data[5]){ if (data[0] < 0 || data[1] < 0 || data[2] < 0 || data[3] < 0 || data[4] < 0) return 0; return data[4] & 0x7F | ((data[3] & 0x7F | ((data[2] & 0x7F | ((((unsigned __int8)data[0] << 7) | data[1] & 0x7F) << 7)) << 7)) << 7);} int main() { int dataX[] = { 0x00, 0x00, 0x0B, 0x22, 0x6C }; int dataY[] = { 0x00, 0x00, 0x03, 0x69, 0x0F }; int dataZ[] = { 0x00, 0x00, 0x18, 0x35, 0x00 }; cout << "X=" << fixed << setprecision(2) << decode(dataX) * 0.001 << endl; cout << "Y=" << fixed << setprecision(2) << decode(dataY) * 0.001 << endl; cout << "Z=" << fixed << setprecision(2) << decode(dataZ) * 0.001 << endl; return 0;}
解密的函数是通过 x64dbg 调试获得的
最后的这个汇编函数就是对应上面解析坐标的C++代码
Node.js 使用坐标判断是否停止
const dgram = require("dgram"); const client = dgram.createSocket("udp4"); let buffer = Buffer.from([0x7d, 0x7d, 0x7d, 0x7c, 0xaa, 0x7d, 0x79, 0x5c, 0xaa, 0x7d, 0x79, 0x4c, 0xaa, 0x7d, 0x79, 0x3c, 0xaa, 0x7d, 0x79, 0x2c]) setInterval(() => { client.send(buffer, 2027, "192.168.102.15", (err, bytes) => { if (err) console.error(err); // console.log(bytes); });}, 2000) let same = false let prePosition = []function isSame(msg) { for (let i = 0; i < msg.length; i++) { if (msg[i] !== prePosition[i]) { prePosition = msg return false } } return true}client.on('message', (msg, rinfo) => { if (isSame(msg)) { if (!same) { // 系统提示音 setTimeout(() => { process.stdout.write('\x07') }, 10) setTimeout(() => { process.stdout.write('\x07') }, 1000) setTimeout(() => { process.stdout.write('\x07') }, 2000) } same = true console.log('位置相同'); } else { same = false console.log('.'); } // client.close();}); client.on("close", () => { console.log("close");});
后面发现了激光切割机在运行的时候加载文档会提示:
所以通过网络抓包获得
发送:7d7d7d7caa7d797daa7d797c
接收:7d7d7d7faa7c797d7d7d7d7d7caa7c797c7d7d7d7d7d
接收的数据其中的第 13 个字节:
0x7d:~(0x7d ^ 0x82) & 0xFF = 0 空闲
0x7c:~(0x7c ^ 0x82) & 0xFF = 1 工作或暂停
改进一下js代码:
const dgram = require("dgram"); const client = dgram.createSocket("udp4"); let buffer = Buffer.from([0x7d, 0x7d, 0x7d, 0x7c, 0xaa, 0x7d, 0x79, 0x7d, 0xaa, 0x7d, 0x79, 0x7c]) function send() { client.send(buffer, 2027, "192.168.101.15", (err, bytes) => { if (err) console.error(err); // console.log(bytes); });} setInterval(send, 2000)send() let preStatus = 0 client.on('message', (msg, rinfo) => { let status = ~(msg[12] ^ 0x82) & 0xFF // console.log('status', status); if (status === 0) { if (preStatus !== status) { // 系统提示音 process.stdout.write('\x07') } console.log('空闲'); } else { console.log('.'); } preStatus = status // client.close();}); client.on("close", () => { console.log("close");});
有了上面获取状态的基础,下一步就可以在界面上做文章了
首先在界面上添加两个标签文本(Static)
我这里使用的是 XNResourceEditor 原因是可以选择控件绘制,至于其他的方式你们都可以尝试,甚至可以使用C/C++ 动态创建
给静态文本设置一个不重复的ID
保存好后,下面开始在程序运行的时候获取到这个“静态文本”的句柄这里使用微软的Spy++ (安装Visual Studio 并且选择使用c++的桌面开发的工作负荷会有自带的,在工具菜单里面)当然也可以自行下载或使用其他的类似工具
点击查找窗口,并把那个定位的图标拖动到你新添加的显示状态的静态文本上方,然后点击OK
从上方看要获取到这个窗口的句柄需要至少5次查找
获取主窗口的句柄Afx:400000:b:10005:6:26060cc7(激光雕刻切割控制系统 V7.92.2 - Untitled) à 获取ProfUIS-DockBar à 获取ProfUIS-ControlBar(控制面板) à 获取#32770(控制面板)à 获取Static(停止)
通常获取窗口会使用 FindWindow API
[DllImport("user32.dll", SetLastError = truestatic extern IntPtr FindWindow(string lpClassName, string lpWindowName);lpClassName 类名 lpWindowName 窗口标题
获取子窗口会使用FindWindowEx API
[DllImport("user32.dll", SetLastError = truepublic static extern IntPtr FindWindowEx(IntPtr parentHandle, IntPtr hWndChildAfter, string className, string windowTitle);parentHandle 父窗口句柄 hWndChildAfter前一个子窗口句柄 className类名 windowTitle窗口标题
经过实验主窗口标题和类名都是会改变的,所以获取的时候都不能FindWindow
使用进程的MainWindowHandle来代替
下面为C#代码
var processList = Process.GetProcessesByName("laserCAD");if (processList.Length > 0) { IntPtr hwn = processList[0].MainWindowHandle; IntPtr t = FindWindowEx(hwn, IntPtr.Zero, "ProfUIS-DockBar", null); IntPtr t2; do { t2 = FindWindowEx(hwn, t, "ProfUIS-DockBar", null); if (t2 != IntPtr.Zero) t = t2; } while (t2 != IntPtr.Zero); t = FindWindowEx(t, IntPtr.Zero, "ProfUIS-ControlBar", "控制面板"); t = FindWindowEx(t, IntPtr.Zero, "#32770", "控制面板"); t2 = FindWindowEx(t, IntPtr.Zero, "Static", "当前状态:"); IntPtr statusHwn = FindWindowEx(t, t2, "Static", null);}
有了句柄现在可以设置静态文本文字了
使用的 SendMessage API
[DllImport("user32.dll", CharSet = CharSet.Auto)]public static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, uint wParam, StringBuilder lParam);const uint WM_SETTEXT = 0x000C; SendMessage(statusHwn, WM_SETTEXT, 20, new StringBuilder("你好"));
这样就可以开始写UDP发送和接收数据了
使用定时器每一秒发送一次请求
void sendMsg() { // 从程序中获取选定的IP地址 EndPoint point = new IPEndPoint(IPAddress.Parse("192.168.102.15"), 2027); var buffer = new byte[]{0x7d,0x7d,0x7d,0x7c,0xaa,0x7d,0x79,0x7d,0xaa,0x7d,0x79,0x7c}; client.SendTo(buffer, point); EndPoint point2 = new IPEndPoint(IPAddress.Any, 0); //用来保存发送方的ip和端口号 byte[] buffer2 = new byte[1024]; int length = client.ReceiveFrom(buffer2, ref point2); //接收数据报 var status = ~(buffer2[12] ^ 0x82) & 0xFF; if (status == 0) { if (preStatus != status) { // 系统提示 //MessageBox.Show(form,"切割完成"); } Console.WriteLine("空闲"); SendMessage(statusHwn, WM_SETTEXT, 20, new StringBuilder("空闲")); } else { Console.WriteLine("."); SendMessage(statusHwn, WM_SETTEXT, 20, new StringBuilder("运行")); } preStatus = status;}
在定时器中调用
client = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);Thread threadSendMsg = new Thread(sendMsg);threadSendMsg.Start();
经过测试发现在加载文档的时候冲突比较大,所以在加载文档框弹出来的时候不去获取状态
private bool isDocLoadOpen(){ var wn = FindWindow("#32770", "文档加载"); return wn != IntPtr.Zero;}
在定时器开始的时候调用如果true直接return
// 跳过文档加载if (isDocLoadOpen()) return;
在程序中是可以选择哪个IP地址的,所以我们需从程序内存中拿到选择的IP地址
上面的条注释就可以在内存中获取IP地址了,是通过32位10进制存储的
IntPtr ipIndexAddr = IntPtr.Zero;private string getSelectIp() { //if (cacheIp != "") return cacheIp; var _memoryUtils = new MemoryUtils(needList.Last().Id); if (ipIndexAddr == IntPtr.Zero) ipIndexAddr = _memoryUtils.GetMemoryAddress("LaserCAD2.1.exe", 0x1B22E8); log("ipIndexAddr:" + ipIndexAddr); var ipIndex = _memoryUtils.ReadToInt(ipIndexAddr); log("ipIndex:" + ipIndex); //var ipAddr = _memoryUtils.GetMemoryAddress("LaserCAD2.1.exe", 0x1B21EC + 8 * ipIndex); var ipAddr = ipIndexAddr - 0x1B22E8 + 0x1B21EC + 8 * ipIndex; log("ipAddr:" + ipAddr); var ip = _memoryUtils.ReadToInt(ipAddr); log("ip:" + ip); var ipStr = "" + (ip >> 24 & 0xff) + "." + (ip >> 16 & 0xff) + "." + (ip >> 8 & 0xff) + "." + (ip & 0xff); cacheIp = ipStr; return ipStr;}
除此之外,基本功能已经完成,但总是觉得不够完善,比如运行和空闲状态显示红色和绿色区分更为明显,使用DLL的方式是,更为便捷
接下来继续实现
程序中需要改变Static的颜色,从网络上查到的资料,都说需要实现函数重载WndProc,苦于没有源码实现不了(或则说难度比较大),于是乎,想到了HDC自己绘制一个。
var title = new StringBuilder(" 空闲 ");SendMessage(statusHwn, WM_SETTEXT, 20, title);var hdc = GetDC(statusHwn);log("hdc:" + hdc);SetBkColor(hdc, 0xF2F2F2);SetTextColor(hdc, ColorTranslator.ToWin32(Color.Green));var rcTitle = new Rectangle(0, 0, 100, 50);ExtTextOut(hdc, 0, 0, ETO_OPAQUE, ref rcTitle, null, 0, IntPtr.Zero);DrawText(hdc, title, title.Length + 1, ref rcTitle, 0);ReleaseDC(statusHwn, hdc);
效果其实还不错,就是布局刷新后需要下一次等下一次重绘才会有颜色,不过已经够用了下面是一些用到的导入函数
[DllImport("gdi32.dll")]static extern uint SetBkColor(IntPtr hdc, int crColor); [DllImport("gdi32.dll")]static extern uint SetTextColor(IntPtr hdc, int crColor); [DllImport("user32.dll", SetLastError = true)]static extern IntPtr GetDC(IntPtr hWnd); [DllImport("user32.dll")]static extern bool ReleaseDC(IntPtr hWnd, IntPtr hDC); [DllImport("gdi32.dll", EntryPoint = "ExtTextOutW")]static extern bool ExtTextOut(IntPtr hdc, int X, int Y, uint fuOptions, [In] ref Rectangle lprc, [MarshalAs(UnmanagedType.LPWStr)] string lpString, uint cbCount, [In] IntPtr lpDx); [DllImport("user32.dll")]static extern int DrawText(IntPtr hdc, StringBuilder lpchText, int cchText, ref Rectangle lprc, uint dwDTFormat);
至于DLL的方式,因为懒的去写一份C++的代码,所以又整了好几天,问题出在C#的生成的dll没有导出表,重网上得知一个叫 UnmanagedExports 的Nuget包可以实现,但是应该是过时了(有个新的DllExport有兴趣的朋友可以试试),需要.Net 3.5的环境,新版本的VS 需要 Visual Studio BuildTools 2015,还需要一个UnmanagedExportLibrary.zip(项目模板放在C:\Users\x\Documents\Visual Studio 2022\Templates\ProjectTemplates\Visual C# 目录里
)经历一番波折后需要把解决方案的平台设置为x86
系统的语言设置成英语
这样编译出来的DLL才有导出标(早知道选择C++了……,还是太犟了……)
DLL中对获取IP地址的优化(因为在同一进程下)
private static string getSelectIp() { var baseAddress = Process.GetCurrentProcess().MainModule.BaseAddress; log("baseAddress:" + baseAddress); var ipIndexAddr = baseAddress + 0x1B22E8; log("ipIndexAddr:" + ipIndexAddr); var ipIndex = Marshal.ReadInt32(ipIndexAddr); log("ipIndex:" + ipIndex); var ipAddr = baseAddress + 0x1B21EC + 8 * ipIndex; log("ipAddr:" + ipAddr); var ip = Marshal.ReadInt32(ipAddr); log("ip:" + ip); var ipStr = "" + (ip >> 24 & 0xff) + "." + (ip >> 16 & 0xff) + "." + (ip >> 8 & 0xff) + "." + (ip & 0xff); log("ipStr:" + ipStr); return ipStr;}
有了DLL现在需要把DLL添加到程序的导入表中
使用LoadPE工具
开始前记得备份一下
直接将LaserCAD.exe拖拽到LoadPE上方
点击目录
然后点击输入表后面的两个点按钮
点击右键添加导入表
输入DLL的名称和需要导入的API名称,然后点击+号按钮
加入列表后点击确定
这个地址需要在汇编的时候调用
将LaserCAD.exe加载到x32dbg中 点击运行,会停在入口处
F8步过
如果卡在循环里可以点击循环跳转前的下一行
然后点击运行到选区即可跳过循环
一直运行到这里程序处于运行状态中
这里就是我们要插入DLL调用的地方(虽然这个地方不是很理想)
在这个地方先下一个断点,一会再回来,按空格把汇编代码复制到剪贴板,并记录下一行地址
call 0x005310C6
00530840
把滚动条拉向下拉,找到都是00的区块
在这里写入一些汇编代码 pushad 和 popad 保证堆栈平衡
调用的DLL中的init函数的地址为 call dword ptr ds:[ 0x0061F00C]
0x0061F00C 为基值 0x400000 + ThunkRAV(0x21F00C)
然后再跳回之前的下一行地址 jmp 0x00530840
复制pushad 的地址 00540637
再断点里面找到之前下的断点,双击回到原来的地方
修改成 jmp 0x00540637
右键点击补丁
点击修补文件,保存成另外一个文件文名
可以把名字改回来
至此所有的功能已经实现,感谢大家耐心的阅读!
模拟DUP(部分)服务的JS代码
const dgram = require("dgram"); const server = dgram.createSocket("udp4"); let buffer = Buffer.from([0x7d, 0x7d, 0x7d, 0x7f, 0xaa, 0x7c, 0x79, 0x5c, 0x7d, 0x7d, 0x76, 0x5f, 0x11, 0xaa, 0x7c, 0x79, 0x4c, 0x7d, 0x7d, 0x7e, 0x14, 0x72, 0xaa, 0x7c, 0x79, 0x3c, 0x7d, 0x7d, 0x65, 0x48, 0x7d, 0xaa, 0x7c, 0x79, 0x2c, 0x7d, 0x7d, 0x7d, 0x7d, 0x7d]) let bufferStatus = Buffer.from([0x7d, 0x7d, 0x7d, 0x7f, 0xaa, 0x7c, 0x79, 0x7d, 0x7d, 0x7d, 0x7d, 0x7d, 0x7d, 0xaa, 0x7c, 0x79, 0x7c, 0x7d, 0x7d, 0x7d, 0x7d, 0x7d]) // 接收的状态请求const bufferReceiveStatus = Buffer.from([0x7d, 0x7d, 0x7d, 0x7c, 0xaa, 0x7d, 0x79, 0x7d, 0xaa, 0x7d, 0x79, 0x7c]) function equals(msg, buf) { for (let i = 0; i < msg.length; i++) { if (msg[i] !== buf[i]) { return false } } return true} server.on("error", function (err) { console.log("server error:\n" + err.stack); server.close();}); let count = 0 server.on("message", function (msg, rinfo) { console.log("server got: " + msg + " from " + rinfo.address + ":" + rinfo.port); let sendBuff = buffer if (equals(msg, bufferReceiveStatus)) { // 状态 ++count; sendBuff = bufferStatus if (count % 5 == 0) { sendBuff[12] = 0x7d } else { sendBuff[12] = 0x7c } } server.send(sendBuff, rinfo.port, rinfo.address, (err, bytes) => { if (err) console.error(err); console.log(bytes, '发送成功'); });}); server.on("listening", function () { var address = server.address(); console.log("server listening " + address.address + ":" + address.port);}); server.bind(2027);
