aardio - 回调函数的方法解析

本文介绍了在Ardio中稳定回调方法,包括函数指针法、回调消息法和简化指针法。作者详细解释了每种方法的原理、示例和注意事项,以及如何避免回调问题和提升跨语言协作效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

aardio中,不免要与各种dll等外部文件函数进行回调,经过不断测试,总结出几种比较稳定的回调方法,如下:

一、函数指针法:

        这是最直接的方法:aardio 将函数转变为 stdcall 方式,将 stdcall 函数指针传递给 dll ,由 dll 直接调用回调函数。

        dll 传递 int、bool 等类型的参数,比较简单,直接声明和传递即可。

        下面以传递 ansi编码文本( dll传递ansi编码,aardio解析为utf8编码 )为例,由简入繁的解析一下:

        1、声明为字符串 string

		// ansi文本传递方式1:直接声明为字符串 string
        // 因为传递的是 ansi 字符,需要先转码为 utf8,aardio才能正确识别。

        func= function(ansiString){
            var utf8String = ..string.fromto(ansiString,0,65001)
            return 1;
		}

        // 将 aardio 函数,转换为 stdcall 函数:
		stdcall = ..raw.tostdcall(func,"int(string)",this)
        
        // 将 stdcall 函数指针,传递给 dll 调用。
		dll.setCallBack(stdcall);

        2、 声明为指针 pointer

		// ansi文本传递方式2:声明为指针 pointer
        // 因为字符串本身就是传址,所以可以更暴力,直接传指针。

        func= function(PansiString){
            var utf8Para = ..string.fromto(..raw.str(PansiString))
            return 1;
		}

        // 将 aardio 函数,转换为 stdcall 函数:
		stdcall = ..raw.tostdcall(func,"int(int,pointer)",this)
        
        // 将 stdcall 函数指针,传递给 dll 调用。
		dll.setCallBack(stdcall);

        3、 声明为整数 int

		// ansi文本传递方式3:声明为 int
        // 指针也是数值,可以当做int使用,但要这样还原成string:

        func= function(intAnsiString){
            var utf8Para = ..string.fromto(..raw.str(topointer(intAnsiString)))
            return 1;
		}

        // 将 aardio 函数,转换为 stdcall 函数:
		stdcall = ..raw.tostdcall(func,"int(int)",this)
        
        // 将 stdcall 函数指针,传递给 dll 调用。
		dll.setCallBack(stdcall);

 注意事项:

        1、stdcall 函数的存活期:

        // 必须要保证 stdcall 是在 dll 调用期间永久有效的,否则会出现闪退等现象。
        // 通常正确的方式是,将 stdcall 声明为一个不会销毁的变量,拥有正确的存活期。
        // 如:

        func= function(ansiPara){
            return 1;
		}

		stdcall = ..raw.tostdcall(func,"int(string)")
        
		dll.setCallBack(stdcall);


        // 错误方法:

        func= function(ansiPara){
            return 1;
		}

		dll.setCallBack(..raw.tostdcall(func,"int(string)"));

        // 上面的 stdcall 也就是..raw.tostdcall() 存活期就出现问题
        // 因为他扮演了一个临时变量的角色,会随着 setCallBack 函数终止而被回收。
        // 所以会因为 setCallBack 的存活期终止,导致 dll 调用出错。

        2、回调函数返回值:

        因为静态编程语言数据声明时已有默认数据,比如:

  • int 类型默认值为 0
  • bool 类型默认值为 false
  • string 类型默认为 “”

        当 aardio 回调函数无返回值时,dll 通常会有一个初始返回值。        

        所以,aardio 没有返回值的情况下,我一般都给 dll 返回 -1,以免让他误以为 aardio 传递的是 0 或 FALSE。

二、回调消息法:

        dll 给 aardio 发送 _WM_THREAD_CALLBACK 线程回调消息(或其他自定义消息),将函数名称及参数信息,通过消息参数进行传递。

        aardio 接收 _WM_THREAD_CALLBACK 线程回调消息(或其他自定义消息)后,分析消息参数,获取函数名称和函数参数信息后,再由 aardio 负责调用回调函数

        此法被用于aardio 范例程序 \ 调用其他语言 \ Delphi \ 嵌入Delphi控件 例程。

        官方 _WM_THREAD_CALLBACK 的使用请自行查看相关文档。

        当然,我个人认为 aardio 自身解析 _WM_THREAD_CALLBACK 的过程太过于繁琐,使用起来也不直观,我更倾向于自己定义消息参数,Wparam 用于传递回调函数名称,如 “setText”,Lparam用于传递函数参数表,如:`{1,2,"hello world"}`,然后由aardio对消息参数进行简单的解析,基本满足需要了。

        解析方法如下:

		parent.wndproc = function(hwnd,message,wParam,lParam){
			if message = 0xACCE/*_WM_THREAD_CALLBACK*/{
				var func = ..raw.tostring(topointer(wParam)):""
				var para = ..string.fromto(..raw.tostring(topointer(lParam)):"",0,65001)
				if #func?parent[[func]]{
					var rval = parent[func](..table.unpack(eval(para)));
					if rval!==null return rval; 
				}
			}
		}

三、简化指针法:

        结合指针法的直接和消息法的简洁,我总结出一个比较简单、稳定、直接的回调方法。

        在aardio中,只定义一个回调函数,该回调函数由 dll 调用,有两个字符串类型参数,第一个参数是“函数名称",第二个参数是“函数参数”:

		var call = function(Pfuncname,Pfuncpara){
		}
		var stdcall = ..raw.tostdcall(this.call,"int(string,string)")
		dll.setCallBack(stdcall);

        dll 中需要调用某个aardio的函数时,只需要调用这个回调函数,比如:

        stdcall("setCellText","{1,2,`这是第1行第2列的文本`}")
        // 第一个参数"setCellText"是aardio函数名
        // 第二个参数"{1,2,`这是第1行第2列`}"是传递给setCellText()的3个参数,以表的形式给回调函数。

        回调函数收到dll的调用时,这样处理:

		call = function(funcname,funcpara){
				var func = ..string.fromto(funcname,0,65001);
				var para = ..string.fromto(funcpara,0,65001);
				if this[[func]]{
					return this[func](..table.unpack(eval(para)));
				}
		}

        由回调函数 call 解析 dll 想调用的函数名和函数参数,如果 aardio 定义了该函数,则由 call 负责调用该函数,并将参数传递过去,同时将该函数的返回值返回给 dll。

        而 aardio 中 setCellText 函数只需要这样定义:

        setCellText = function(row, col, text){
                ...
                return true;
        }

        // 因为 call 调用此函数时,是使用索引操作符[]调用的,所以此函数的 owner 为空。
        // 所以此函数内不能使用 owner。

        这样,setCellText 函数就间接被 dll 回调了。

        这样做的好处如下:

        1、只需要在 aardio 中定义 一个 回调函数,并将其转为 stdcall 即可。

       2、其它的函数在 aardio 中当做一般函数定义即可,而不用关心转成 stdcall 的问题。

        3、在 dll 中只需用 stdcall 间接调用其它函数即可,而不用去逐个声明。调用方法也很简单,只需提供函数名称和参数表即可。即使这个函数没被 aardio 定义,调用也不会导致程序出错。

4、回调函数模板:

        这里提供我封装dll类时,定义的回调函数,仅作参考:

		this.call = function(funcname,funcpara){
				var func = ..string.fromto(funcname,0,65001)
				var r = null;
				if this[[func]]{ // 如果定义了该函数则调用
					var para = ..string.fromto(funcpara,0,65001)
					if para="{}" { //无参调用该函数
						r = this[func]()
					} else { //有参调用该函数
						r = this[func](..table.unpack(eval(para)))
					}
				}
				return r===null?-1:r;
		}
		this.stdcall = ..raw.tostdcall(this.call,"int(string,string)")
		this.dll.setProp(this.tag,"callback",this.stdcall);

5、如果你觉得这个方法不错,是不是可以叫他 “光庆回调法” 。

哈哈 ^_^。

<think>我们使用aardio语言通过串口控制吉时利2700设备,并处理返回数据。主要步骤包括: 1. 使用sio库进行串口通信 2. 发送SCPI指令控制设备 3. 使用软件流控(XON/XOFF) 4. 参数化触发延迟 5. 实时读取数据并保存到Excel 6. 开发用户界面 根据引用[5]中提到aardio有sio库用于串口通信,我们可以使用sio库。 步骤: 1. 打开串口:配置串口参数(波特率、数据位、停止位、流控等) 2. 设置中断函数(可选)用于接收数据(引用[5]提到设置中断函数) 3. 发送SCPI指令(注意SCPI指令以换行符结束,通常是`\n`) 4. 读取返回数据(可能需要根据设备返回的数据格式进行解析) 5. 将数据实时保存到Excel(可以使用aardio的com库操作Excel) 6. 设计用户界面(可以使用aardio的窗体设计器) 注意:吉时利2700的串口参数一般为:波特率9600,数据位8,停止位1,无奇偶校验,使用软件流控(XON/XOFF)。 具体实现: 1. 串口配置和打开: 使用`sio`库的`sio.open`函数打开串口,并设置参数。 2. 软件流控:在打开串口时设置流控模式为`sio.ixoff`(输入使用XON/XOFF)和`sio.ixon`(输出使用XON/XOFF)。 3. 发送指令:使用`sio.write`发送字符串,注意SCPI指令需要以换行符结束。 4. 接收数据:可以使用`sio.read`函数读取,或者设置中断回调函数(在数据到达时自动调用)。 5. 触发延迟参数化:在发送触发指令前,可以设置延迟时间(使用`win.delay`函数)。 6. Excel保存:使用com库创建Excel对象,将数据写入。 7. 界面开发:使用win.form创建窗口,添加按钮、文本框等控件。 示例代码框架: ```aardio import sio; import com; import win; import fsys; import inet.http; // 创建界面 var winform = win.form(text="吉时利2700控制";right=800;bottom=600) winform.add( buttonOpen={cls="button";text="打开串口";left=20;top=20;right=120;bottom=50}; buttonStart={cls="button";text="开始采集";left=20;top=70;right=120;bottom=100}; buttonStop={cls="button";text="停止采集";left=20;top=120;right=120;bottom=150}; buttonSave={cls="button";text="保存数据";left=20;top=170;right=120;bottom=200}; editLog={cls="edit";left=140;top=20;right=780;bottom=580;multiline=true;vscroll=true} ) // 全局变量 var serialPort = null; var excelApp = null; var excelBook = null; var excelSheet = null; var isCollecting = false; // 打开串口 winform.buttonOpen.oncommand = function(id,event){ if(serialPort){ win.msgbox("串口已经打开"); return; } // 配置串口参数 serialPort = sio.open("COM1", // 串口号,根据实际情况修改 sio.baud9600, sio.databits8, sio.stopbits1, sio.parity_none, sio.ixoff | sio.ixon // 软件流控 ); if(!serialPort){ win.msgbox("打开串口失败"); return; } // 设置中断函数(回调函数)用于接收数据 serialPort.cnt_irq = function(port, count){ // 当有数据到达时,count是接收缓冲区中的字节数 var data = port.read(count); // 读取数据 // 处理数据,例如显示在编辑框 winform.editLog.text = winform.editLog.text ++ data; // 同时可以解析数据并保存到变量,用于写入Excel } winform.editLog.text = "串口已打开\n"; } // 开始采集 winform.buttonStart.oncommand = function(id,event){ if(!serialPort){ win.msgbox("请先打开串口"); return; } isCollecting = true; // 发送SCPI指令,例如设置触发延迟(参数化,这里假设延迟时间为0.5秒) var delayTime = 0.5; // 延迟时间,可以做成界面输入 serialPort.write("TRIG:DELAY " + delayTime + "\n"); // 发送开始采集指令,例如触发一次扫描 serialPort.write("INIT\n"); winform.editLog.text = winform.editLog.text ++ "开始采集...\n"; } // 停止采集 winform.buttonStop.oncommand = function(id,event){ isCollecting = false; winform.editLog.text = winform.editLog.text ++ "停止采集\n"; } // 保存数据到Excel winform.buttonSave.oncommand = function(id,event){ // 创建Excel对象 excelApp = com.CreateObject("Excel.Application"); excelApp.Visible = true; excelBook = excelApp.Workbooks.Add(); excelSheet = excelBook.Worksheets(1); // 假设我们有一个数据表,这里简单示例,实际应从接收的数据中获取 var data = { {"时间", "值1", "值2"}, {1, 2.3, 4.5}, {2, 3.4, 5.6} }; for(i=1; #data; 1){ for(j=1; #data[i]; 1){ excelSheet.Cells(i, j).Value2 = data[i][j]; } } // 保存文件 var filePath = fsys.dlg.save("保存文件",,"Excel文件(*.xlsx)|*.xlsx"); if(filePath){ excelBook.SaveAs(filePath); } excelBook.Close(false); excelApp.Quit(); excelApp = null; } // 关闭串口(在窗口关闭时) winform.wndproc = function(hwnd, message, wParam, lParam){ if(message == 0x10 /* WM_CLOSE */){ if(serialPort){ serialPort.close(); } if(excelApp){ excelApp.Quit(); excelApp = null; } } } winform.show(); win.loopMessage(); ``` 注意: 1. 以上代码是框架,实际使用需要根据吉时利2700的SCPI指令手册调整指令。 2. 数据解析部分需要根据设备返回的数据格式进行,例如可能是CSV格式或者固定格式。 3. 串口回调函数中,读取的数据可能包含多条记录,需要解析每条记录(例如以换行符分隔)。 4. 保存到Excel时,需要将解析后的数据存储到数组中,然后写入。 关于触发延迟参数化:我们可以在界面上添加一个输入框,让用户输入延迟时间,然后在发送指令时使用该值。 改进:添加一个输入框用于设置触发延迟时间。 在界面上添加: ```aardio winform.add( staticDelay={cls="static";text="触发延迟(秒):";left=20;top=220;right=120;bottom=240}; editDelay={cls="edit";text="0.5";left=120;top=220;right=200;bottom=240} ) ``` 然后在开始采集时获取这个值: ```aardio var delayTime = tonumber(winform.editDelay.text); if(!delayTime){ delayTime = 0.5; // 默认值 } ``` 参考文献: [^1]: 串口通信流程 [^5]: aardio串口通信示例 由于吉时利2700的具体SCPI指令需要参考其手册,这里只给出通用指令示例。 相关问题: 1. 如何确定吉时利2700的串口参数和SCPI指令格式? 2. 在aardio中如何处理串口接收数据的解析(特别是连续数据流)? 3. 如何优化实时数据采集和保存到Excel的性能? 4. aardio中如何实现多线程以避免界面卡顿? 5. 如何实现软件流控(XON/XOFF)的具体控制?</think>### 使用aardio控制吉时利2700的串口通信方案 #### 1. **串口配置与连接** 使用`sio`库配置串口参数(参考引用[1][^1]): ```aardio import sio; var comPort = sio.open("COM3", 9600, 8, 1, 0); // 波特率9600, 8位数据, 1停止位, 无校验 comPort.flowcontrol = sio.ixoff | sio.ixon; // 启用软件流控(XON/XOFF) if(!comPort) error("串口打开失败"); ``` #### 2. **SCPI指令控制** 发送设备控制指令(需参考吉时利2700手册): ```aardio // 设置触发延迟(参数化) delayTime = 0.5; // 单位:秒 comPort.write("TRIG:DELAY " + delayTime + "\n"); // 启动测量 comPort.write("INIT\n"); // 读取数据 comPort.write("FETCH?\n"); ``` #### 3. **数据接收与解析** 实时读取并解析返回数据(参考引用[5][^5]): ```aardio // 设置中断回调函数 comPort.cnt_irq = function(port, count) { var data = port.read(count); if(data) processData(data); // 自定义数据处理函数 } // 帧结构解析示例 processData = function(rawData) { var values = string.split(rawData, ","); // 按逗号分割数据 if(#values >= 3) { var voltage = tonumber(values[1]); var current = tonumber(values[2]); saveToExcel(voltage, current); } } ``` #### 4. **Excel数据保存** 使用`com`库操作Excel: ```aardio import com.excel; excel = com.CreateObject("Excel.Application"); excel.Visible = true; workbook = excel.Workbooks.Add(); sheet = workbook.ActiveSheet; // 表头设置 sheet.Cells(1,1).Value2 = "时间"; sheet.Cells(1,2).Value2 = "电压(V)"; sheet.Cells(1,3).Value2 = "电流(A)"; rowIndex = 2; // 数据起始行 saveToExcel = function(voltage, current) { sheet.Cells(rowIndex, 1).Value2 = time.now(); sheet.Cells(rowIndex, 2).Value2 = voltage; sheet.Cells(rowIndex, 3).Value2 = current; rowIndex++; } ``` #### 5. **用户界面开发** 创建控制界面(按钮/输入框/数据显示): ```aardio import win.ui; var winform = win.form(text="吉时利2700控制", width=600, height=400); // 触发延迟输入框 winform.add("editDelay", {cls="edit"; left=20; top=20; width=100; text="0.5"}); // 按钮控件 winform.add("btnStart", {cls="button"; text="开始采集"; left=20; top=60}); winform.add("btnSave", {cls="button"; text="保存Excel"; left=120; top=60}); // 数据显示区域 winform.add("listView", { cls="listview"; left=20; top=100; width=560; height=280; gridLines=true; columns={"时间", "电压(V)", "电流(A)"} }); // 按钮事件 winform.btnStart.oncommand = function() { delayTime = tonumber(winform.editDelay.text); comPort.write("TRIG:DELAY " + delayTime + "\n"); comPort.write("INIT\n"); } ``` #### 6. **关键参数说明** | 参数 | 推荐值 | 说明 | |--------------|-------------|-------------------------| | 波特率 | 9600 | 设备默认通信速率 | | 流控 | XON/XOFF | 软件流控防止数据丢失[^1] | | 帧结束符 | `\n` | SCPI标准指令终止符 | | 触发延迟范围 | 0.001~3600s | 根据设备手册设定 | ### 注意事项 1. **SCPI指令规范**:需查阅吉时利2700手册确认具体指令格式 2. **错误处理**:添加串口超时检测和指令响应验证 3. **实时性优化**:使用多线程处理数据采集和界面更新 4. **数据完整性**:通过校验和验证数据包(如CRC)[^1] [^1]: 串口通信需要配置参数包括波特率、数据位、停止位等,并建议使用错误检测机制如CRC校验。 [^5]: aardio支持通过`sio.cnt_irq`设置中断回调函数实时处理串口数据。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卢光庆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值