利用PC机,通过串口协议控制单片机或其他嵌入式系统是工业上广泛使用的技术。相信很多人都没用网页开发过串口应用程序,近期我尝试使用HTML+JavaScript+ActiveX模式开发了一个简单的串口控制应用程序。
示例应用程序,点击这里进入下载页面。
(可看到完整的网页应用程序代码,运行之前需要注册控件,不过我只能“口头”保证没有病毒。)
关于“网页应用程序(HTA)”的基本介绍,大家搜一下就知道了,很简单的。
ActiveX控件在这个架构中完成串口通信的功能,在网页上,一个控件代表一个串口连接,如果需要控制多个串口,则需要多个串口空间。把串口读写功能封装在一个ActiveX控件里并不困难,在此也就不罗嗦了。我写的串口通信控件提供了这么几个接口:
[code=C/C++]
/**
** 描述:使用类似"9600,n,8,0"的设置字符串设置串口
** 参数: port 串口号
** 参数: setstr 串口状态设置,如“9600,n,8,0”
** 返回: 0(失败),1(成功)
**/
LONG CommOpen(LONG port, LPCTSTR setstr)
/**
** 描述:发送串口数据
** 参数: data 发送的数据,其格式为以字符串描述的十六进制的字节,其序列以空格隔开
** 返回: 0(成功)
**/
LONG CommSend(LPCTSTR data);
/**
** 描述:判断串口是否已经被打开
** 返回:true(打开),false(未打开)
**/
VARIANT_BOOL IsCommOpen(void);
/**
** 描述:关闭串口
**/
void CommClose(void);
[/code]
当串口收到数据时,通信控件会携带收到的数据触发一个数据接收事件:
[code=C/C++]
/**
** 描述:串口数据接收事件
** 参数:data 接收的数据,其格式为以字符串描述的十六进制的字节,其序列以空格隔开
**/
OnCommRecv(LPCTSTR data);
[/code]
在HTA应用程序中捕捉并处理这个时间的方式如下:
[code=HTML]
<object id="ComAxCtrl" classid="clsid:0-0-0-0-0" style="width:1px; height:1px;"></object>
<script language="javascript" for="ComAxCtrl" event="OnCommRecv(data)">
OnCommRecv(data);
</script>
[/code]
<object>在程序中加入了一个串口通信控件,并取名为“ComAxCtrl”,<script for=… event=…>定义了该事件的处理脚本。
基本技术就是这些。下面就是关于如何使用javascript封装串口协议了。
[code=javascript]
// 串口命令帧类型定义,仅做示例:帧头(0xA5)+数据+校验和(CS)+帧尾(0x5A)
function Command( d )
{
this.SS = "A5";
this.DD = d;
this.CS = this.CheckSum();
this.ZZ = "5A";
}
Command.prototype.CheckSum = function()
{
var sum = 0;
sum = parseInt( this.SS, 16 );
var arr = this.DD.split(//s+/);
var i = 0;
for( i=0; i<arr.length; i++ )
{
sum += parseInt( arr[i], 16 );
}
sum = sum & 0xFF;
return Byte2String( sum );
}
Command.prototype.GetCommandLine = function()
{
return this.SS + " " + this.DD + " " + this.CS + " " + this.ZZ;
}
[/code]
然后写2个实用函数,构造常用指令
[code=javascript]
// "电机初始化" 命令
function MotorInitCommand( id )
{
var dd = "A0 DD " + id;
return (new Command( dd ) );
}
// "查询电机离起始位置步数" 命令
function MotorAbsPosQuerryCommand( id )
{
var dd = "C0 AF" + id;
return (new Command( dd ) );
}
[/code]
接下来就是命令管理类了,用于发送指令、处理接收数据:
// 串口命令超时处理函数
function OnCommandTimeout( )
{
CommCommandQueue.DefaultHandler(); // 调用命令队列(全局对象)的默认处理函数
}
//#############################
// 串口命令管理器
function CommCommand( cmd, desc, timeout, rcv_hdl, to_hdl )
{
this.Cmd = cmd; // 命令对象
this.IsHandled = false; // 命令是否被处理
this.Desc = desc; // 命令描述,用于调试
this.TimeOut = timeout; // 命令超时值
this.Timer = null; // 定时器
this.RecvDataHandler = rcv_hdl; // 数据处理函数指针
this.DefaultHandler = to_hdl; // 命令超时处理函数指针
}
// 发送命令
CommCommand.prototype.Send = function( )
{
this.IsHandled = false;
clearInterval( this.Timer );
CurrentRecieveData = ""; // 清空“接收缓冲区”(全局对象)
ComAxCtrl.CommSend( this.Cmd.GetCommandLine() );
this.Timer = setInterval( this.DefaultHandler, this.TimeOut );
}
// 串口数据处理 -- 处理入口函数
CommCommand.prototype.RecvHandler = function( recvdata )
{
if( true==this.IsHandled )
return;
this.IsHandled = true;
clearInterval( this.Timer );
if( null!=this.RecvDataHandler )
{
var recvarr = recvdata.split( //s+/ );
// 接收的数据帧长度一定大于6
// "A5 DD CS 5A"
if( recvarr.length>=4 )
{// 接收数据处理
this.RecvDataHandler( recv_frame );
}
}
}
上面提到了两个全局对象,一个是命令队列对象,一个是“接收缓冲区”,它们是这么定义的:
// 全局数据定义
var CommCommandQueue = new CommandQueue();
var CurrentRecieveData = ""; // 当前接收的命令回复帧
看来,其实所谓的“接收缓冲区”就是字符串了。
那CommandQueue类又是怎么定义的呢?代码如下。正如名字说明的,这个类封装了一个“命令队列”,通过PushCommand方法将准备执行的命令插入队列,调用Run方法即可顺序执行队列里的命令。在队列收到下位机回传的数据后,将通过RecvHandler或DefaultHandler方法处理,在这两个处理函数的末尾,均将调用RunNext,使得队列里的下一个命令能够执行。因此这个命令队列里的命令都是以“发送命令à等待回传数据à处理回传数据à发送下一命令……” 这样的方式处理的。
// 串口命令队列
function CommandQueue()
{
this.CmdArray = new Array();
this.CurrentCommCommand = null; // 当前处理的命令
this.IsCompleted = true;
}
CommandQueue.prototype.PushCommand = function( cmd )
{
this.CmdArray.push( cmd );
this.Run();
}
CommandQueue.prototype.GetCurrentCommCommand = function()
{
return this.CurrentCommCommand;
}
CommandQueue.prototype.Run = function()
{
if(null!=this.CurrentCommCommand && false==this.CurrentCommCommand.IsHandled)
{
return;
}
this.CurrentCommCommand = this.CmdArray.shift();
this.CurrentCommCommand.Send();
}
CommandQueue.prototype.RunNext = function()
{
if( 0>=this.CmdArray.length )
{
this.CurrentCommCommand = null;
}
else
{
this.Run();
}
}
CommandQueue.prototype.RecvHandler = function(recvdata)
{
this.CurrentCommCommand.RecvHandler(recvdata);
this.RunNext();
}
CommandQueue.prototype.DefaultHandler = function()
{
if( null==this.CurrentCommCommand ) return;
this.CurrentCommCommand.IsHandled = true;
clearInterval( this.CurrentCommCommand.Timer );
this.RunNext();
}
有了这些工具,现在可以封装我们应用相关的一些概念了。假如我们要控制的是步进电机(Motor),以初始化电机为例:
// 电机控制类
function Motor( id, steps, steplen, speeds )
{
this.ID = id; // 电机编号:X/Y/Z
this.TotalSteps = steps; // 电机导轨长度(步数)
this.LenPerStep = steplen; // 单位步长(μm)
this.SpeedArray = speeds; // 各档速度
this.Speed = 12; // 速度档,默认12档
this.AbsPos = 0; // 电机当前的绝对位置(步数)
this.ZeroPos = 0; // 电机“零点”的绝对位置(步数)
this.CommandProxy = null; // 串口命令处理
// UIMotorInfo是关于界面显示元素的控制的一层封装
this.UICtrls = new UIMotorInfo( this.ID, this.LenPerStep );
}
// 初始化电机,注意变量 me 的“闭包性”
Motor.prototype.Initialize = function()
{
var me = this;
this.CommandProxy = new CommCommand(
MotorInitCommand(this.ID), ""+this.ID+"电机初始化", 300
,function(rcvfrm){me.InitRecvFun(rcvfrm);}
,function(rcvfrm){me.InitTimeoutFun();} );
CommCommandQueue.PushCommand( this.CommandProxy );
}
Motor.prototype.InitRecvFun = function( recvfrm)
{
this.UICtrls.SetPositions( 0 );
}
Motor.prototype.InitTimeoutFun = OnCommandTimeout;
如电机移动、查询步进电机位置等操作按类似方式实现即可。而对于整个数据处理过程的开始,还是从最开始说到的OnCommRecv函数开始,当某个用户操作,构造了一系列命令对象后,正是这个函数,驱动着命令队列一个一个地执行队列中的命令:
// 串口接收数据处理函数
function OnCommRecv(data)
{
var commcmd = CommCommandQueue.GetCurrentCommCommand();
if( null!=commcmd )
{
CurrentRecieveData += (data + " ");
// 判断接收缓冲区中是否是一个完整的命令,如果是,则执行队列的数据处理函数
if( IsAnValidCommand( CurrentRecieveData ) )
{
//alert( "接收到数据帧:"+CurrentRecieveData );
CommCommandQueue.RecvHandler( CurrentRecieveData );
}
}
}