【AllJoyn专题】基于AllJoyn和Yeelink的传感器数据上传与指令下行的研究

接触高通物联网框架AllJoyn不太久,但确是被深深地吸引了。在我看来,促进我深入学习的原因有三点:一、AllJoyn开源,对开源的软硬件总会有种莫名的喜爱,尽管也许不会都深入下去;二、顺应潮流,物联网虽远未普及,但已是大势所趋,高通公司在领域布局,致力于打造舒适高效的智能家居场景,推出AllJoyn软件框架,适应了发展趋势;三、文档丰富,开源软件的使用,特别是框架,若没有文档相助,相信没有多少开发者愿意尝试,AllJoyn在这方面做得不错,日后还需做得更好。当然啦,也有些额外原因,包括高通的大力推广,个人对C++的喜爱等等。

 

最近,根据之前所学,利用AllJoyn和国内受人欢迎的Yeelink物联网平台完成了一个简单的Web of Things的小系统。我们知道随着因特网的蓬勃发展和物联网在全球的兴起,一个新的运行模式也在悄然诞生,即Web of Things,简称为WoT。它可被理解为是IoT的一部分,集中实现以Web方式来控制和管理物联网中的资源,包括各种网关及网关上的传感器,其主旨是提倡通过REST Web API的形式直接对智能终端与网关上的资源进行开放,用户可以通过访问互联网的方式来访问终端的数据资源,这就是典型的互联网模式。而Yeelink平台恰好能提供这样的功能需求,所以我选择了它作为应用层;而在网络层可以细分为两种,一种是公网传输,即借助目前成熟的互联网,二种是局域网传输,AllJoyn与生俱来的局域传输能力就在这里得到了体现;最下面为感知层,即Arduino终端作为网关接各种感知设备。结构示意图如图0所示:


目前系统实现的两大功能如下:

1、上传温度传感器采集的温度值至Yeelink平台,在平台上以易读方式显示;

2、通过点击平台上的虚拟开关向感知层的Arduino终端发出命令,控制LED灯的亮灭;


1 工具和开发环境

AllJoyn

关于AllJoyn的介绍,我相信维基百科和官方文档会比我说得详细得多,可参考后文的链接。按照我目前的理解就是用它可以实现邻近设备间的互联互通,不管是什么设备,只要支持alljoyn,通过wifi、蓝牙都可快速连接,实现信息共享和及时通信。它的好处之一就在于支持多编程语言和多平台,非常方便开发者的使用


Yeelink

Yeelink是一个国内开放的的物联网平台,每个注册用户都可免费添加设备及传感器,利用平台提供的Restful接口,实现对各个传感器的代码访问,从而可以实现传感器数据上传和控制终端等多种功能。有这样一个免费平台,相信对开发者来说是一大福音!

 

软件环境

我目前服务端是在windows 7系统下做此实验,如若在linux环境下运行,需修改部分平台相关代码。集成开发环境是Visual Studio 2012,很强大的IDE,在x86平台下用scons命令生成的samples文件夹下同样都是VS项目文件。客户端的实现则是用开源硬件流行的IDE——arduino-1.5.6-r2,它支持arduino due开发板,用它可进行文件的编辑与烧写。

 

硬件环境

除了x86 PC,大点的就只是arduino due开发板了。日前智能硬件的盛行也促进了开源硬件领域的发展,用arduino等相关成熟硬件可快速做系统原型,大量节约成本,在适当情况下是个很好的技术解决方案。令开发者欣慰的是,开源硬件社区非常流行,所以有很好的问题解决资源。

有arduino板,但无传感器可不行。为方便起见,我目前所展示的就只是温度传感器DS18B20一种。由于具体的传感器数据获取与alljoyn并无关联,所以就以温感为例阐述基于alljoyn的数据传输,其它传感器数据就与之类似了。另外,为了配合控制指令下传,配备了一个发光二极管,当然这是arduino due板上已有的,在13号引脚上。


---------------------------------------------------------------------------------------------------- --------------

友情建议:建议初学者学习x86平台下的alljoyn时,先可直接在VS下进行编辑生成,毕竟有比较好的代码提示功能,熟练后再可用notepad等工具。如若刚学就在notepad上写代码,会很让人无奈,因为一大堆函数和参数你都不知道,不容易发现错误。

---------------------------------------------------------------------------------------------------- --------------


2 结构框架

本系统共有两个Arduino Due开发板作客户端,由于是瘦客户端,所以需要标准客户端提供Daemon才可连接,这一点在官方文档中讲得很明白,不再赘述;Windows 7 PC端作为服务端,发布服务供瘦客户端连接,也许有朋友注意到了这与官方例子ledctrl和AJ_LedService不太一样,客户与服务的角色颠倒了,瘦客户端不再作为服务而是客户了;另外一方面,PC服务端通过互联网与Yeelink平台进行交互,实现数据上传与接收指令,接收到的指令又通过AllJoyn总线控制瘦客户端,从而实现了Yeelink平台下基于AllJoyn的数据传输与控制功能。其结构框图如图1所示:



3 各子系统详解


3.1 Yeelink平台

要想利用Yeelink资源,就须在官网注册一个唯一帐号,在用户中心进行设备和传感器添加。如下图所示,我添加了arduino设备


接下来在“我的设备”项,添加温度传感器和控制开关,系统会为每一个设备生成唯一的URL,通过URL就可以访问特定传感器了。具体操作文档可参考这里:http://www.yeelink.net/develop/api


3.2 PC服务端

在讲到接下来的服务和客户实现时,我会就核心代码作详细解析,而不会写上完整代码,望读者理解

服务端主流程如下图所示:


上图是主线程的流程,由于在PC上是多线程运行,所在在监听对象、总线对象上都有额外的线程运行,它们是异步的,也就意味着当事件发生时,可以迅速得到响应,比如当服务端收到温感瘦客户端传来看温度时,总线对象就调用其方法处理函数向Yeelink平台上传。下面就重点细节来谈谈如何设计服务端


首先创建总线对象,然后给总线对象添加接口。接口中有一个sendTemp方法,带一个字符串输入参数,有一个ledSwitch信号,带一个uint8_t型参数。最后激活接口和启动总线

g_msgBus = newBusAttachment("myapp", true);
InterfaceDescription* ledIntf = NULL;
status =g_msgBus->CreateInterface(::org::alljoyn::alljoyn_test::InterfaceName,ledIntf, false);
ledIntf->AddMethod("sendTemp","s",NULL,"instr",0);
ledIntf->AddSignal("ledSwitch","y","inbyte",0);
ledIntf->Activate();
status = g_msgBus->Start();  


接下来创建监听和总线对象,分别给总线注册监听和总线对象实例,最后总线实例开始连接本地router

g_busListener = new MyBusListener;
g_msgBus->RegisterBusListener(*g_busListener);
MyObj obj(*g_msgBus,"/temp");
pobj = &obj;
g_msgBus->RegisterBusObject(*pobj);
g_msgBus->Connect();

在监听类中,我们重新实现了几个虚函数,包括

bool AcceptSessionJoiner(SessionPortsessionPort, const char* joiner, const SessionOpts& opts)

void SessionJoined(SessionPort sessionPort,SessionId id, const char* joiner)

void NameOwnerChanged(const char* name,const char* previousOwner, const char* newOwner)

前两个是服务端特有的,当有客户接入时,会自动被调用;最后一个服务和客户都可调用,当有服务或客户进入或退出时,总线上会发生名称改变,从而它被调用,有时不止一次。这三个虚函数的实现基本是常规写法,就不在此讲解了

 

在总线对象实例的构造中,我是这样做的:

MyObj(BusAttachment& bus,const char* path):BusObject(path)
{
	const InterfaceDescription* intf = bus.GetInterface(::org::alljoyn::alljoyn_test::InterfaceName);
	AddInterface(*intf);
	
	const MethodEntry methodEntries[] = {
		{ intf->GetMember("sendTemp"), static_cast
   
   
    
    (&MyObj::sendTemp) }
	};
	QStatus status = AddMethodHandlers(methodEntries, sizeof(methodEntries) / sizeof(methodEntries[0]));


	ledSwitchMember = intf->GetMember("ledSwitch");
}

   
   

首先给总线添加已经设置的接口,为sendTemp方法添加方法处理函数,同时给私有成员ledSwitchMember设置值


在方法处理函数中,获取传过来的温度值,字符串形式,就往yeelink平台上传:

void sendTemp(const InterfaceDescription::Member* member, Message& msg)
{
	static int num = 0;
	const char* tempstr = msg->GetArg(0)->v_string.str;
	printf("\n%dth Receive 'sendTemp' method call:%.4s\n",++num,tempstr);
	sendToYeelink(tempstr);
}
void sendToYeelink(const char* tempstr) 
{
	int socket_id;

	makeString(tempstr);//拼装组成http post请求

	initWinSockAndConnect(&socket_id);//初使化socket
	printf("Sending to Yeelink...\n");
	send(socket_id , http_request, strlen(http_request), 0);//模拟发送post请求

	char http_response[1024] = {0};
	int bytes_received = 0;
	bytes_received = recv( socket_id , http_response, 1024, 0);
	http_response[ bytes_received ] = '\0';

	// 判断是否收到HTTP OK
	char* presult = strstr( http_response , "200 OK\r\n");
	if( presult == NULL ) printf("Http response error\r\n");
}

我需要强调的是,在makeString方法中,若要正确拼装成http post请求,有几条属性不能少,于是定义了四个全局数组:

char yeelink_server[] = "api.yeelink.net";
char temp_path[] = "/v1.0/device/9966/sensor/19877/datapoints";
char switch_path[] = "/v1.0/device/9966/sensor/22595/datapoints";
char apikey[] = "d3d565a5923afdd82105e0e5a";

对应着post请求中的以下项:

POST /v1.0/device/9966/sensor/19877/datapoints HTTP/1.1

Host: api.yeelink.net

U-ApiKey: d3d565a5923afdd82105e0e5a

host和path共同组成了传感器的URL,详细说明可参见yeelink文档


至于在初使化windows socket函数中,也需用到yeelink_server,填入相关结构的域,如下所示:

void initWinSockAndConnect(int* psocket)
{
	WSADATA wsaData;
	int result;
	
	result = WSAStartup(MAKEWORD(2, 2), &wsaData);
	if (result != 0) {
		printf("WSAStartup failed: %d\n", result);
		return;
	}

	// DNS解析 获得远程IP地址
	struct hostent *remote_host;
	remote_host = gethostbyname(yeelink_server);
	if( remote_host == NULL )
	{
		printf("DNS failed\r\n");
		return;
	}

	// 创建套接字
	*psocket = socket(AF_INET, SOCK_STREAM, 0);
	struct sockaddr_in remote_sockaddr;
	remote_sockaddr.sin_family = AF_INET;
	remote_sockaddr.sin_port = htons(80);
	remote_sockaddr.sin_addr.s_addr = *(u_long *) remote_host->h_addr_list[0];
	memset(&(remote_sockaddr.sin_zero), 0, sizeof(remote_sockaddr.sin_zero));
	// 连接远程主机
	result = connect( *psocket, (struct sockaddr *)&remote_sockaddr, sizeof(struct sockaddr));
	if( result == 0 )
	{
		//printf("connect ok\r\n");
	}
}

这部分是与平台相关的,若移植到linux平台,需要修改


在总线对象类中,还有一个成员函数emitLedSwitchSignal用于主线程发射信号给led瘦客户端

QStatus emitLedSwitchSignal(uint8_t ledbyte)
{
	MsgArg arg("y",ledbyte);
	printf("sending signal...\n");
	return Signal(NULL,ledclientId,*ledSwitchMember,&arg,1,0,0);
}	

将发来的参数封装成message参数,和信号一起发送出去,注意sessionid为led客户端id


我们回到主线程main中,Connect之后就开始发布服务了,三步曲:Request,CreateSession,Advertise

const TransportMask mask = TRANSPORT_ANY;
g_msgBus->RequestName(::org::alljoyn::alljoyn_test::DefaultWellKnownName, DBUS_NAME_FLAG_DO_NOT_QUEUE);

SessionOpts opts(SessionOpts::TRAFFIC_MESSAGES, true, SessionOpts::PROXIMITY_ANY, mask);
SessionPort sp = 250;
g_msgBus->BindSessionPort(sp, opts, *g_busListener);

g_msgBus->AdvertiseName(::org::alljoyn::alljoyn_test::DefaultWellKnownName, mask);

最后进入循环,轮询我在yeelink中添加的控制开关状态:

while (true) 
{	
	ledState = getSwitchState();//轮询开关状态
	if (switchChanged)		
	{        		
	    //如果状态改变就发射信号
		switchChanged = false;
		printf("led new state:%d\n",ledState);
		pobj->emitLedSwitchSignal(ledState);
	}
	Sleep(300);//不能太大,否则延迟大,灯过了很久才响应; 但小了,请求频率高又会被yeelink所拒绝
}

在这里我采取的是被动轮询开关状态的方式,其实不是最佳的,最好是开关状态一改变,就像硬件中断似的,立刻通知CPU,而在此之前CPU完全可以去做其它事。但这需要yeelink平台的主动发送,貌似不太好办,所以就隔断时间轮询状态了。时间间隔也要选好,大了,LED灯变化有延迟;小了,请求太频繁又被yeelink拒绝。那么如何轮询呢?其实与上传温度差不多,还是组装(不过现在是GET请求)、初使化socket,不过在接收中我是这么做的:

int bytes_received = 0;  
bytes_received = recv( socket_id , response , 1024 , 0);  
response[ bytes_received ] = '\0';  

if (strstr(response,"value") == NULL)//如果找不到value,说明响应失败,暂默认返回0
	return 0;

// 用fiddler等软件模拟请求可知,状态的改变值就在字符串“value”开始的第7号位上
newch = *(strstr(response,"value") + 7);
if (newch != oldch) {
	// 如果新状态不等于旧状态
	printf("led switch state changed from %c to %c\n",oldch,newch);
	switchChanged = true;//设定状态改变标志,布尔值
	oldch = newch;// 保存新状态
}
return (newch - 48);//字符1和0与数字相差48

相关注意点已在注释中说明。之所以待状态改变后才发射信号,也是为了性能着想,没有必要在状态未改变时也发射。最后返回的值是一个uint8_t型,取值为0或1,0表示熄灭LED灯,1表示点亮LED灯。只要开关状态改变,就把此信息发送给LED瘦客户端


3.3 温感瘦客户端

写客户端代码之前,首先建立文件夹,起名需和.ino文件的主名保持一致,这是arduino环境默认的习惯。我将着重讲述温感数据的发送过程,至于获取温度值,读者可参考arduino中文社区的这个帖子:http://www.arduino.cn/thread-1345-1-1.html,同时下载四个源文件:

DallasTemperature.cpp,DallasTemperature.h,OneWire.cpp,OneWire.h,这属于获取温度的库,全部和ino文件放在一起

另外创建另外一个cpp文件,为alljoyn相关的核心文件,传输温度值。下面着重讲述这个文件

首先要重视以下几个数据结构的书写:

static const char ServiceName[] = "org.alljoyn.service.test";
static const char ServicePath[] = "/temp";
static const uint16_t ServicePort = 250;

static const char* const sampleInterface[] = {
    "org.alljoyn.intf.test",   //接口名
    "?Dummy foo
   
   
   
   

服务名、路径、端口、接口名务必和服务端保持一致,接口中必须要有sendTemp方法,携带一个字符串参数,其它的和Dummy作用一样,作填充;SEND_TEMP表示为AJ_PRX_MESSAGE_ID型


在AJ_Main中,首先做如下工作:

AJ_Initialize();//瘦客户端初使化
AJ_PrintXML(AppObjects);//打印接口,以xml形式
AJ_RegisterObjects(NULL, AppObjects);//注册对象

while (!done) {
	AJ_Message msg;

	if (!connected) {
	// 启动客户端
		status = AJ_StartClient(&bus,NULL,CONNECT_TIMEOUT,FALSE,ServiceName,ServicePort,&sessionId,NULL);

		if (status == AJ_OK) {
		//如若在串口终端不能打印字符,可尝试将AJ_InfoPrintf换成AJ_Printf,同时减少一对()
			AJ_Printf("StartClient returned %d, sessionId=%u.\n", status, sessionId);
			connected = TRUE;

		} else {
			AJ_Printf("StartClient returned 0x%04x.\n", status);
			break;
		}
}

成功连上服务后,StartClient才会返回


接下来才是核心动作,获取温度、方法调用、睡眠,再循环:

sensors.requestTemperatures();
temp = sensors.getTempCByIndex(0);//获取到温度值,float型

MakeMethodCall(&bus,sessionId,temp);//进行sendTemp方法调用

status = AJ_UnmarshalMsg(&bus, &msg, UNMARSHAL_TIMEOUT);
if (AJ_OK != status){
  AJ_Printf("AJ_UnmarshalMsg error\n");

}
if (AJ_ERR_TIMEOUT == status) {
	continue;
}
if (AJ_OK == status) {
	switch (msg.msgId) {
		case AJ_REPLY_ID(SEND_TEMP):
		{
			AJ_Printf("rece the reply of SEND_TEMP\n");
		}
		break;	                      
		case AJ_SIGNAL_SESSION_LOST_WITH_REASON:
		/* A session was lost so return error to force a disconnect. */
		{
			uint32_t id, reason;
			AJ_UnmarshalArgs(&msg, "uu", &id, &reason);
			AJ_AlwaysPrintf(("Session lost. ID = %u, reason = %u", id, reason));
			 connected = FALSE;
		}
		status = AJ_ERR_SESSION_LOST;
		break;

		default:
		/* Pass to the built-in handlers. */
			status = AJ_BusHandleBusMessage(&msg);
		break;
	}
}

/* Messages MUST be discarded to free resources. */
AJ_CloseMsg(&msg);
	  
if (status == AJ_ERR_SESSION_LOST) {
	AJ_Printf("AllJoyn disconnect.\n");
	AJ_Disconnect(&bus);
	exit(0);
}
AJ_Sleep(10000);//等上一段时间再发送	

由于我不关心后面的解消息过程,所以重点就是前几句,方法调用如下:

void MakeMethodCall(AJ_BusAttachment* bus, uint32_t sessionId,float temp)
{
    AJ_Status status;
    AJ_Message msg;
    char tempstr[5];

    status = AJ_MarshalMethodCall(bus, &msg, SEND_TEMP, ServiceName, sessionId, 0, METHOD_TIMEOUT);

	sprintf(tempstr,"%f",temp);
  
    if (status == AJ_OK) {
        status = AJ_MarshalArgs(&msg,"s",tempstr);
    }

    if (status == AJ_OK) {
        status = AJ_DeliverMsg(&msg);
    }
   if (status == AJ_OK) {
      AJ_Printf("\send temp ok\n");
    }
}

首先marshal方法,然后marshal参数,即温度值,最后deliver。这样,就会导致服务端的方法回调函数被调用


3.4 LED瘦客户端

这一端与温感有些地方类似,不过文件就只有2个,一个ino,一个以alljoyn为主的cpp

主要修改在于在sampleInterface中,添加ledSwitch信号"!ledSwitch instr>y",再预定义#define LED_SWITCH  AJ_PRX_MESSAGE_ID(0, 0, 2)

开始阶段当然就与温感类似了,StartClient成功后,就开始解消息,因为服务端的信号要过来了:

AJ_Printf("waiting for ledswitch signal...\n");
status = AJ_UnmarshalMsg(&bus, &msg, UNMARSHAL_TIMEOUT);
if (AJ_OK == status){
  AJ_Printf("msgid:%u\n",msg.msgId);
}
if (AJ_ERR_TIMEOUT == status) {
	continue;
}
if (AJ_OK == status) {
	switch (msg.msgId) {
		case LED_SWITCH:
		{ 
			AJ_Arg arg;
			status = AJ_UnmarshalArg(&msg, &arg);
			AJ_Printf("rece ledswitch signal\n");

			uint8_t* ledbyte=arg.val.v_byte;

			if (AJ_OK == status) {
				AJ_Printf("'%s' (path='%s') received '%u'.\n", ServiceName, 
						  ServicePath, *ledbyte);
				if (*ledbyte == 1)
					doLed(HIGH);
				else
					doLed(LOW);
				 
			} else {
				AJ_Printf("AJ_UnmarshalArg() returned status %d.\n", status);
			}		
			break;
		}
		...
	}
}

当信号过来后,检验消息ID,发现是LED_SWITCH,就解参数,获取开关状态。如果为1,则点亮LED;为0则熄灭之。另外在这个客户端就不用睡眠了,因为是被动接收服务端的信号


4 演示验证

待服务、两个客户端代码实现后,服务端生成exe文件,连接好硬件,客户端分别烧入两个arduino due开发板。粗略图如下图:


那个红色板上面就有DS18B20温度传感器,三个引脚号接入到了其中一个arduino板的引脚上。两个板子都与主机PC通过路由器同处一个局域网内部。还得强调一点,若要使PC服务端能与板子通信,必须启动瘦客户端SDK bin下的SampleDaemon.exe程序,因为服务端并没有绑定的Daemon,它就是来提供Daemon给瘦客户端使用的,其源码可以\alljoyn-14.02.00-src\alljoyn_core\samples\SampleDaemon找到

下面首先看温度传感器的验证

连接好硬件上电,打开yeelink平台的温度传感器的显示页面,方便随时刷新;点击arduino IDE,按shift+ctrl+M打开串口终端,有如下显示:


如若没有SampleDaemon,是不会有最后一句输出的;接下来在命令行执行服务端程序:



正如上图所示,服务端一启动,就发现了客户接入,接入成功之后开始会话。由于我的开关初使状态是开着的,程序默认为关,所以状态改变就发射了一次信号;同时收到了温感一端发来的温度值;串口终端的打印也表明传送温度成功,字符'f'的输出是我的输出函数的小问题,暂不管它。几秒钟之后,成了这样:


咦?为什么温度上升了呢?呵呵,那是因为我把手指放在DS18B20上了!它当然温度升高啦。此时我们再来看yeelink上温感的反应:


不负所望,在14年9月13日 12:26:34分显示了最初的温度值26.6,前面的为之前的数据了。这也就实现了在线监控温度的功能了


在另外一客户端,打开串口窗口:



服务端收到了另外一客户的接入,NameOwnerChanged被调用了好几次,倘若我接下来在用鼠标点击yeelink的控制开关,即下图所示:


服务和瘦客户端的反应是:



由结果可知,我是开了一次开关,又关了一次开关,导致客户端先后接到1,0,过程中观察LED灯的反应就是一亮一灭,同时温度值也可在下图中收到:


上面应该是传输错误才出现了a字符

到此为止,整个预先设想功能基本实现了


5 值得改进

1、可以添入更多传感器,从而让功能更加丰富

2、可以让第三个瘦客户端充当服务,比如用arduino板,移动手机等等,如果处理能力满足条件的话。因为作为多个客户的服务端,数据处理能力应该要强些,如果只是单线程,像arduino板,能否处理值得验证

3、可以给温度传感器设定阈值,一旦温度超过给定值就采取报警,鸣响蜂鸣器之类的设备


6 参考链接

AllJoyn官方1:https://allseenalliance.org/         

AllJoyn官方2:https://www.alljoyn.org/

Yeelink官网:http://www.yeelink.net/

Arduino Due介绍:http://www.arduino.cc/en/Main/ArduinoBoardDue

Arduino 中文社区:http://www.arduino.cn/


评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值