@gnat-xj
2015-11-17T02:17:16.000000Z
字数 7618
阅读 2215
notes docs bcd
read online:
程序只有一个,运行在两个电脑上,一个为 Master,一个为 Slave,TCP 连接建立后。操作 Master 的 UI,执行可能在
对 UI 而言,无所谓执行发生在哪台电脑(实现细节被 Wrappers 屏蔽)。
Wrapper 主要做了两件事:
Wrapper 和 Class 有几乎相同的调用接口,
所以原来在单电脑上的程序只要简单修改即可。
对程序的要求:
主程序运行后,先要选择自己是 Master(高性能平台)还是 Slave(工控机):

高性能平台要先打开,并打开 TCP 网络服务器:(点击 Serve)

再运行程序,不过这次选择 Slave,然后将工控机连接到高性能平台:(点击 Connect)

现在两者连接上后,界面发生变化(一些按钮可以使用,一些按钮不再能使用)。现在点击下方大按钮,可以同步两者的颜色(在红和绿直接切换)。
关闭这两个界面会弹出一个 demo 界面,展示的如何同步两者的 UI 以及运算如何分布在两台电脑上,如图:

可以看到,
同时,界面是同步的。下面介绍如何将原有类改善,实现跨主机通讯、UI 同步。
先明确几个概念,
这里的修改指南指导了如何在 Class 和 UI 之间插入一个封装,在不改变(几乎)原有类的基础上实现跨主机通信。
比如从 ui->pushButton->setText( class->getString() ) 改起。
首先,在 QString Class::getString( args ) 中 emit 一个信号:
emit wrp_getString( returnValue ) // 要提前定义(signals: void wrp_getString( QString text );)
然后参照 Class 定义一个 Wrapper,提供 void Wrapper::getString( args ) 函数(几乎和原有类 Class 一模一样),
把 Class 的信号和 Wrapper 的槽连接:
connect( class, SIGNAL(wrp_getString(QString)),wrapper, SLOT(onWrp_getString(QString)) );
Wrapper 构造的时候会传入 Class 类实体指针,
如果 Class 在本机运行,Wrapper::getString( args ) 会直接调用 Class::getString( args ),
如果 Class 在异地运行,经过判断 Wrapper::getString( args ) 会把函数名 ID 和参数 args 传到另一台电脑,
由另一台电脑的对应的 Wrapper 的 Class 实体执行 Class::getString( args )。
不管哪个电脑上执行了 Class 函数,信号都会被那个电脑的 Wrapper 捕获,然后 emit 一个同样的信号,并让另一台电脑 emit 这个信号。
即 Wrapper::getString( args ) 的大致逻辑为:
// Class 函数对应的 Wrapper 函数void Wrapper::getString( args ){if ( Class registered on THIS_COMPUTER ){// 本机执行this->class->getString();}else{// 异地执行(把函数名和参数传到另一台电脑)send `Class::getString' + `args' to the other computertell THE_OTHER_COMPUTER to run: class->getString()}}// 信号所连接的槽void onWrp_getString( QString returnValue ){// 本机释放信号emit wrp_getString( returnValue );// 让异地电脑释放信号tell THE_OTHER_COMPUTER to: emit Wrapper::wrp_getString( returnValue )}
现在,两台电脑上,有一台电脑执行了操作(可能是你通过 UI 交互的那一台,或者是另一台),
释放了两个同样的信号,只要把这个信号绑定到各自的 UI,界面就同步了:
connect( wrapper, SIGNAL(wrp_getString(QString)),ui, SLOT(onWrp_getString(QString)) );
最后 UI 得到这个信号,在界面上有所反映:
void Ui::onWrp_getString( QString returnValue ){ui->pushButton->setText( returnValue );}
现在,
需把原来的 ui->pushButton->setText( class->getString() ) 换成 wrapper->getString()。
总结一下,原来的一句之间的 setText,变成了间接的:
Class 函数Class emit 信号给 WrapperWrapper 在两台电脑 emit 信号给 UIUI 得到信号,同步更新界面注意:
直接调用 private 成员也不可以了。
所有的 Wrappers 和 UI 都在一个全局的 Bundle 中绑定。(源码位于 Src/Wrappers/Wrappers.h)
/** 获得实例*/Bundle::getInstance(); // 获得唯一的、静态的一个实例,包含了所有的大的类实例,UIs,Wrappers(Classes)/** 本机是高性能平台(Master)还是工控机(Slave)?*/// who am I?Bundle::whoAmI(); // 我是谁?MASTER 还是 SLAVE?// 可以进行判断if ( Bundle::whoAmI() == MASTER ) {qDebug() << "我是高性能平台(Master)";}/** Bundle 的东西有*/// 主要模块的“封装”Bundle::getInstance()->lms // LMS WrapperBundle::getInstance()->lmsAgent // Wrapper 用到的界面Bundle::getInstance()->server // 网络通信接口Bundle::getInstance()->client // 网络通信接口...// 也可以获得主要模块的“类实体”(不推荐直接调用)Bundle::getInstance()->lms->kernel // LMSReader 实体,可能为 NULL(如果不在本机注册).../** Bundle 的一些函数*/// 向另一台电脑发数据Bundle::getInstance()->send( const QByteArray &msg ); // 发送数据到另一台电脑// 数据需要满足一定的格式,可通过一个 Moderator(“翻译”)静态类/函数进行转化,如:// 要在另一台电脑上运行 LMSReader::genNewPath( "D://tmp/" ); (LMS 注册在那台电脑上)Bundle::getInstance()->send( Moderator::lms_genNewPath( "D://tmp/" ) );
最近两天的工作)很简单,修改 Moderator 的一个 HashMap 即可:
// Master: 高性能平台// Slave : 工控机wss.insert( BCD::TYPE_LMS, SLAVE );wss.insert( BCD::TYPE_MCU, SLAVE );wss.insert( BCD::TYPE_UR, SLAVE );wss.insert( BCD::TYPE_ARM, SLAVE );wss.insert( BCD::TYPE_SP20000C, MASTER );wss.insert( BCD::TYPE_MULTIPLICATION_ON_SLAVE, SLAVE );wss.insert( BCD::TYPE_ADDITION_ON_MASTER, MASTER );
加了一些注释,精简了代码。
UR0 封装了 URController,源码位于 Src/Wrappers/UR0.h。
class UR0 : public QObject{Q_OBJECTpublic:URController *kernel; // kernel 变量为封装的对象private:bool doItYourself;public:UR0( URController *ur0 = NULL );
构造函数基本如下:
UR0::UR0( URController *ur0 /* = NULL */ ){// 类执行实体kernel = ur0;// 判断是否在本机执行doItYourself = Moderator::wss.value( BCD::TYPE_UR ) == Bundle::whoAmI();/** 说明:* Moderator::wss: 记录了所有模块的注册信息(在高性能执行,还是在工控机),* 新加的模块也要去注册,源码在 `Src/Utils/moderator.cpp'* Bundle::whoAmI(): 本机是高性能还是工控机?*/// 如挂在本机执行,但执行实体为 NULL,说明没有正确初始化if ( doItYourself && NULL == kernel ) {Logger::log() << "Fatal Error";exit( EXIT_FAILURE );}// 连接“实体”原有的信号到“封装”的槽,重新 emit 信号 & 分发到两台电脑connect ( kernel, SIGNAL(wrp_getLocalAddress(QString)),this, SLOT(onWrp_getLocalAddress(QString)) );}
这个 wrp_getLocalAddress(QString) 信号是对 QString URControllor::getLocalAddress() 的封装,在保持原有类返回值的情况下,在函数内部 emit 信号,这里连接后被 UR0 捕捉,重新发射信号(在两台电脑):
void UR0::onWrp_getLocalAddress( QString addr ){// 在本机 emit 信号(本机界面可以捕获)emit wrp_getLocalAddress( addr );// 在异地 emit 信号(异地界面可以捕获)Bundle::send( Moderator::sig_ur_wrp_getLocalAddress( addr ) );}
Bundle::send 会把函数和参数传到另一台电脑,再次发射信号。
所有的 Moderator 的函数(如这里的 Moderator::sig_ur_wrp_getLocalAddress( QString ))
要现在 Moderator 里指定一个函数 ID(用于区分不同函数),ID 命名规则为:模块名__FUNCTION_NAME__TYPE1_TYPE2
举例:
// ur 模块,genNewPath 函数,QString 类型参数UR__GEN_NEW_PATH__QSTRING// ur 模块,genNewPath 函数,无类型参数也要显式地表明 VOIDUR__GEN_NEW_PATH__VOID// 信号的模块名为 SIG_模块名__函_数_名__参_数_名,如SIG_UR__WRP_GET_LOCAL_ADDRESS__QSTRING// lms 模块,setFrequencyAngleresolution 函数,参数为 int、doubleLMS__SET_FREQUENCY_ANGLERESOLUTION__INT_DOUBLE// 上面的函数 ID 对应的函数依次为://// static QByteArray ur_genNewPath( const QString &path );// static QByteArray ur_genNewPath( )// static QByteArray sigUR_wrp_getLocalAddress( const QString &addr )// static QByteArray lms_setFrequencyAngleresolution( const int i, const double &d )
指定了函数 ID 后要把对应的函数和参数序列化为一串字节,需要定义一个静态函数,返回值为 QByteArray,
比如上面的 UR__GEN_NEW_PATH__QSTRING,对应的函数为:
// .h 文件中申明static QByteArray ur_genNewPath( const QString &path );// .cpp 文件中实现QByteArray Moderator::ur_genNewPath( const QString &path ){TX_OUT << (int)UR__GEN_NEW_PATH__QSTRING // 传入函数 ID,强制转化为 int 型<< path; // 传入调用的参数return tx; // 这里的宏 TX_OUT 和 return tx 不必深究}
这样就能顺利地用 Bundle::send( Moderator::ur_genNewPath( path ) ) 把信息传递到另一台电脑。
不过,还得写解析部分。对收到的网络数据,要分析是要调用什么函数,还要还原参数,这部分在 void Moderator::dispatch( QByteArray &msg ) 函数中定义,
比如上面的 Moderator::ur_genNewPath( const QString &path ) 在 dispatch 函数中就是:
// 已经提取了函数 ID 到 flag// 如果函数 ID 为 UR__GEN_NEW_PATH__QSTRINGelse if ( UR__GEN_NEW_PATH__QSTRING == flag ){// 参数为 QString,那就定义一个 QStringQString path;// 从网络中取出 QString 类型的路径in >> path;// 让本地的“类执行”实体去调用这个函数Bundle::getInstance()->ur->genNewPath( path );/** 补充说明:* 这里也可直接调用 `Bundle::getInstance()->ur->kernel->genNewPath( path )'* 但并不推荐这样做(Wrapper 为判断调用是否合法,kernel 不会)。*/}
然后,把模块加到 Bundle 里,比如 UR,则到 Bundle 类中加入变量:
// 加入变量class Bundle{...UR0 *ur;...};// 初始化,在Bundle Bundle::initInstance( ){Bundle bundle;...bundle.ur = NULL;...return bundle;}// 在 main.cpp 中初始化(参考 `Src/MasterSlave/main.cpp')Bundle::getInstance()->ur = Bundle::whoAmI() == Moderator::wss.value( BCD::TYPE_UR )? new UR0( new URController ): new UR0;
封装流程完毕。
需要做的事情有:
定义函数 ID、序列化函数、解析和 dispatch