5.1 机床模型的建立
通常,复杂的机床模型应该使用专业的建模软件进行建模,而不是由程序直接生成[12]。在我们的仿真系统中,机床的模型由弯管机生产厂商提供Solidworks模型,这就产生了格式转换和转换后模型重心定位、重新命名的问题。
为了仿真的需要,Solidworks建立的机床模型需要提供简化的机床模型。简化的机床模型是真实机床部件的一个适当的简化,以最小的资源消耗达到仿真和干涉检查的需要。
5.1.1 几何建模
机床的主要简化模型和部件名称见表 51,其中英文名称作为程序中检索用。
|
表 51 机床简化模型及部件名称
|
|
序号
|
模型
|
英文名称
|
对应类名
|
|
1
|
床身
|
MACHINE
|
Machine
|
|
2
|
小车
|
CARRIER
|
Carrier
|
|
3
|
主轴
|
MAIN_AXIS
|
MainAxis
|
|
4
|
主夹具
|
MAIN_CLAMP
|
MainClamp
|
|
5
|
辅助夹具
|
AUX_CLAMP
|
AuxClamp
|
|
6
|
机头
|
HEAD
|
Head
|
|
对于各个基本的部件,简化的要求是:
1. 保留:体积较大外露部件和移动部件。包含床身、油缸、夹具、小车(以及夹头)、电机、油缸、轨道、转轴、伸缩件。
2. 忽略:
油管:机头可能和管子发生干涉的、不会弯曲的固定油管除外。
可移动、柔性的连接线:包括各种电缆。
细小部件:包括各种螺丝、铆钉。
3. 尺寸要求:
按照机床实际尺寸提供
根据以上要求,弯管机厂商提供了简化的Solidworks机床模型如图 51
|

|
|
图 51 机床简化模型
|
5.1.2 格式的转换和模型的二次修改
Solidworks提供的模型无法直接在OpenSceneGraph系统中使用,为了能够顺利的使用其模型,需要进行模型格式的转换。
我们决定在3DSMAX中对模型进行二次处理,利用OpenSceneGraph的OSG、IVE导出插件进行导出。3DSMAX支持很多种文件格式的导入,最为常用的是3DS格式。但很遗憾,Solidworks无法导出成为3DS格式。
经过尝试,决定使用VRML格式作为中介,将Solidworks装配好的机床导出到*.WRL文件(VRML格式),然后导入到3DSMAX中。
导入到3DSMAX后,需要对坐标系进行更正。因为导入的所有的模型元件都以世界坐标系原点作为原点。对于需要绕轴进行旋转的部分,必须要进行坐标系的重定义。方法是在3DSMAX中选择模型后,选择工具栏中的Hierarchy,Affect Pivot Only,然后重新定义其坐标系到转轴即可。
对于所得的模型,按照表 51进行命名,再导出为OSG、IVE格式就得到了程序所支持的机床模型。
5.2 建立模型部件和C++类的关系
根据面向对象的思路,将每一个模型都建立成单独的C++类,每一个类和一个机床部件进行关联,这样可以操控单独的类,也方便以后的扩展[17]。
在这个系统中,由表 51所列的五大部件分别单独建立C++类,这些类都继承自PartBase类。PartBase类提供了操控机床部件的一些常用函数:
Move:移动
Rotate:绕定义的轴旋转
SetMatrixTransform,GetMatrixTransform:设置、获取对应的模型部件的osg::Group,也可通过直接改变变换矩阵进行定位等操作。
SetName,GetName:设置或获取名字
这些函数都定义为虚函数,继承类可根据实际需要改动这些函数的实现。
Machine类定义了机床所有的部件的一个列表:
struct PartList
{
AuxClamp* auxClamp;
Carrier* carrier;
Head* head;
MainAxis* mainAxis;
MainClamp* mainClamp;
InstalledPipe* installedPipe;
};
这个列表包含了机床的所有部分,还包括一根安装在机床上的管子InstalledPipe。InstalledPipe类也继承自PartBase,但还有一些辅助函数:
SetPipe,GetPipe:用于设置一个管子的实例
RebuildPipe:加工后管子形状会发生变化,调用这个函数重新生产管子的几何信息。
disableRebuild,ManualRebuild:由于重放操作的需要而设置。disableRebuild设置为True的时候可以跳过RebuildPipe步骤,在回放完成需要绘制管形的时候再执行ManualRebuild进行重画,提升处理速度。(参见5.4.4节)
Machine类还定义了一个参数列表,用于设置机床的各个参数:
struct ParameterList
{
float HoldMainClampVelocity; //主夹紧的速度
float HoldMainClampMovement; //主夹紧的移动量
float ReleaseMainClampVelocity; //主夹松的速度
float ReleaseMainClampMovement; //主夹松的移动量
float HoldAuxClampVelocity; //辅夹紧的速度
float HoldAuxClampMovement; //辅夹紧的移动量
float ReleaseAuxClampVelocity; //辅夹松的速度
float ReleaseAuxClampMovement; //辅夹松的移动量
float ResetVelocity; //转臂回转速度
float zMoveVelocity; //c轴转速
std::vector<float> DiePosition; //模具的Z轴位置
};
Machine类还定义了一个状态列表:
struct StatusList
{
bool IsBending;
};
IsBending状态指示现在机床的状态。在IsBending为True的时候,机床各个部件可以根据预先定义的流程执行弯管操作,而为False的时候,机床部件不进行弯管。
5.3 调入机床模型并初始化
OpenSceneGraph提供了很简洁的方式调用一个模型文件:
osg::Node* machineModel=osgDB::readNodeFile("machine.ive");
osgDB::readNodeFile首先查找相应的插件,然后通过插件解析模型文件,最后返回一个osg::Node的指针,这个指针就包含了机床部件之间的层次关系。只需要将这个osg::Node作场景根root的子Node即可:root->addChild(machineModel);
管子作为场景的一部分也需要进行初始化和定位,在这里专门设置CreatePipe函数进行管子的初始化工作:
void CreatePipe()
{
if(pipe!=NULL) delete pipe;
pipe=new Pipe();
pipe->Create(2.519,366.5); //建立一个r=2.519,l=366.5的管子
installedPipe->SetPipe(pipe); //设置机床安装的管子为这根管子
pipeMt=pipe->GetPipe(); //得到管子的几何模型
osg::Matrix matrix=pipeMt->getMatrix();
matrix.setTrans(osg::Vec3(21.079,-135.938,46.918)); //设置管子的安装点
pipeMt->setMatrix(matrix);
installedPipe->SetMatrixTransform(pipeMt.get()); //设置机床安装的管子的几何模型
}
同样,作为场景的一部分,管子的几何模型也需要加入到场景中:root->addChild(pipeMt.get());
接下来对machine->parameters的各个参数进行详细设置:
float moveMent=3;
machine->parameters.HoldAuxClampMovement=moveMent;
……
machine->parameters.zMoveVelocity=20;
machine->parameters.DiePosition.push_back(0);
machine->parameters.DiePosition.push_back(-8.992);
DiePosition定义了模具的Z坐标位置,需要从上到下进行设置。
接下来需要对机床的各个部件的模型按照到虚拟机床上:
第一步需要找到机床的模型并为部件设置具体的模型,例如:auxClamp->SetMatrixTransform(FindMatrixTransform(root,"AUX_CLAMP"));
FindMatrixTransform从 root中检索出名字为AUX_CLAMP的模型,然后将AuxClamp类的实例auxClamp的模型设置为此模型即可。其他部件类似。对于类提供的额外的参数,需要进行进一步详细的设置:
auxClamp->IsAuxClampHold=true; //辅夹夹紧
carrier->installedPipe=installedPipe; //设置小车上的管子
ainClamp->IsMainClampHold=true; //主夹夹紧
最后一步将部件的"安装"到机床上:
machine->parts.auxClamp=auxClamp;
……
machine->parts.mainClamp=mainClamp;
这就完成了机床的初始化操作。
5.4 工艺模拟
5.4.1 命令类
为了实现机床的控制,我们结合机床的实际,将YBC的数据转换成操作机床的数控代码。仿真系统所用的数控代码和操作机床所用的数控代码保持一致。
主要代码包括:
AT_YMOVE:进料
AT_BMOVE:转料
AT_DOB:弯料
AT_ROLL:推弯
AT_MOVE_HEAD:机头横移
AT_BENDRETURN:转臂复位
AT_HOLD_MAIN_CLAMP:主夹紧
AT_RELEASE_MAIN_CLAMP:主夹松
AT_HOLD_AUX_CLAMP:辅夹紧
AT_RELEASE_AUX_CLAMP:辅夹松
AT_HOLD_CARRIER_CLAMP:小车夹头紧
AT_RELEASE_CARRIER_CLAMP:小车夹头松
AT_AUXPUSH_FORWARD:辅推进
AT_AUXPUSH_BACKWARD:辅推退
AT_SHIFT_DIE: 换模
这些操作指令加上操作指令所带的数据,即可操作机床。
为了实现这些指令在仿真机床上的模拟和以后的扩展,我们使用了Command设计模式[3]。Command类定义了所有命令类的基类。timeRef用于记录一个命令执行的时间。Execute是一个纯虚函数,每一个继承自Command的类都需要单独定义相应的Execute函数。它用于执行命令,返回值为True说明命令已经执行完毕。对于需要并发的一系列命令,通过AddChild、DelChild和GetChilds函数进行命令的增加、删除和获取操作。
例如AT_YMOVE指令,其结构如下:
class AT_YMOVE : public Command
{
public:
AT_YMOVE():machine(NULL){};
AT_YMOVE(TAG_YMOVEDATASTRUCT yMoveData,Machine* machine)
{ this->yMoveData=yMoveData;
this->machine=machine;
}
virtual bool Execute(float simulateTime);
TAG_YMOVEDATASTRUCT yMoveData;
Machine* machine;
};
外界给AT_YMOVE指令提供Y轴的运动数据,其数据结构如下:
struct TAG_YMOVEDATASTRUCT {
double dblDistance; //送料长度(distance between bend)
double dblVelocity; //送料速度
double dblOffset; //送料偏移量
};
Execute函数首先计算单位时间内的移动量
float movement=yMoveData.dblVelocity*simulateTime;
然后根据timeRef计算总共移动的量
float totalMoved=yMoveData.dblVelocity*timeRef;
如果总共移动量大于送料长度,返回true
if(abs(totalMoved)>=yMoveData.dblDistance){ return true; }
如果还需要移动,控制machine的carrier部分移动movement
else {machine->parts.carrier->Move(osg::Vec3(0,movement,0)); }
完成命令以后,timeRef+= simulateTime
5.4.2 命令序列类
为了将一系列命令进行统一的处理,我们创建了CommandSequence类,这个类依然继承于Command类,在此基础上增加了AddCommand( Command* cmd)函数,用于在命令序列中增加一个命令。std::vector<Command*>保存所有的命令,std::vector<Command*>::iterator lastCmd用于记录最后一条执行的命令。
|

|
|
图 52 建立脚本
|
当执行命令序列的Execute函数后,就调用lastCmd的Execute函数并记录返回值到cmdFinished变量。然后通过一个循环过程,依次调用lastCmd的子命令的Execute函数,并记录下返回值到cmdChildFinished中。如果命令本身和所有的子命令都返回True,则说明这个命令已经执行完成,lastCmd指向下一个命令,并如此重复直到到达最后一个命令。此时CommandSequence的Execute函数会返回True,指示所有的命令已经完成。
如果需要重新开始加工模拟,则只需要调用Reset函数,此函数会依次调用所有的命令的Reset函数,并将timeRef赋值为0。然后再调用CommandSequence的Execute函数即可。
5.4.3 建立和执行脚本
当建立好控制弯管机的指令以后,便可翻译工艺文件,建立执行脚本,并进行仿真。基本思路如图 52。通过调用CommandSequence::Execute()函数可以仿真一帧的时间,默认情况下,按照60帧/秒的速度进行。在主循环内,只需要重复调用此语句即可实现仿真模拟。每60帧作为现实中的一秒。如果还要考虑到和现实时间同步,则需要计算每秒的帧数。
5.4.4 仿真回放的实现
|

|
|
图 53 仿真倒转流程
|
对于需要仔细观看的步骤,有必要进行回放,此功能类似于普通播放机的倒带功能。要实现回放,通常来说有以下思路:
1、对所有的部件的运动或者形变进行记录。类似于录像的功能。但要实现录像功能需要消耗很多的内存,当加工步骤越来越多时,所需要的内存容量也呈线性增长。
2、为每一个命令书写倒放的函数,将运动倒放。此方案需要重新书写函数,而且可能有些运动无法实现精确的倒放。如果从倒放点开始继续加工可能会造成积累误差。
对于我们的系统,由于使用了命令序列的设计,而且每一帧都是执行相同的函数,模拟相同的时间片。从模拟开始执行N次Execute()函数所得到加工的结果完全相同。为了实现倒放T帧,我们只需要在CommandSequence复位以后,执行Execute()函数N-T次即可。具体过程见图 53。
此法简单可行,节约了大量内存,也没有修改任何命令的实现。但由于加工时间越长,N越大,倒转T帧以后N-T次Execute()的函数调用,每次调用又要涉及到工艺的计算,计算量呈线性增长的趋势。在实际操作中,我们发现每次执行Execute()后需要对管子的形状进行建模,这个过程很消耗时间,所以将建模过程取消,只记录管形数据即可。这也就是5.2节中设置disableRebuild和ManualRebuild的原因。管形建模的过程放在绘制之前进行,这就很好的解决了计算量的矛盾,实际应用当中效果理想。