UPNP编程
安装SDK相对比较简单,参考阅读SDK目录下的README
我使用命令如下:
tar jxvf libupnp-1.6.17.tar.bz2
cd libupnp-1.6.17/
./configure –prefix=/home/momo/DLNA –enable-sample
make
make install
这样在/home/momo/DLNA目录下就可以找到include和lib两个目录了,里面就是头文件和库,在upnp/sample目录下是示例程序。
交叉编译目前没有去理会,先学习下X86下面UPNP的编程。
另外可以在网上下载intel upnp tools来对自己编写的设备进行测试。
一、名词解释和XML文档
UDN:uuid,设备唯一名
SID:订阅标识,唯一
ServiceId:服务ID号,唯一
URI:Universal Resource Identifier
UPNP编程分为设备端和客户端(控制点),设备端采用XML来提供自己的信息,主要包括自身描述XML文档和动作状态文档。
自身描述XML文档格式:
见文档。
动作状态XML文档格式:
见文档。
这些XML的编写,除了自己按照格式填之外,还可以使用Intel upnp tools中的DeviceBuild和ServiceAuthor生成。我测试DeviceBuild好像有点问题,无法保存。
其中自身描述XML文档有个节点presentationURL,这个主要用来表现设备的界面,是一个网址,网页是自己编写的。
二、XML的操作
编写设备或控制点都需要操作XML。
下面是一个普通的XML:
<serviceList>
<service>
<serviceType>urn:schemas-upnp-org:service:service_0001:1</serviceType>
<serviceId>urn:upnp-org:serviceId:0001</serviceId>
<controlURL>/upnp/control/service_0001</controlURL>
<eventSubURL>/upnp/event/service_0001</eventSubURL>
<SCPDURL>/service_0001SCPD.xml</SCPDURL>
</service>
</serviceList>
节点是XML中一个很重要的概念,对于理解后面操作XML有帮助,下面这些都是节点:
<xxx>
<yyy>xxxxxx</yyy>
</xxx>
<serviceType>urn:schemas-upnp-org:service:service_0001:1</serviceType>
urn:schemas-upnp-org:service:service_0001:1
其中有<>的节点是Element节点,节点分类比较多,详情自己百度下。
1.API
int UpnpDownloadXmlDoc(const char *url, IXML_Document **xmlDoc)
下载url指向的XML文档,保存在*xmlDoc中。
IXML_NodeList *ixmlDocument_getElementsByTagName(
IXML_Document *doc,
const DOMString tagName)
从一个XML文档中读取所有标签名是tagName的节点,并把它们编制成一个单向链表。
IXML_Node *ixmlNodeList_item(
IXML_NodeList *nList,
unsigned long index)
从链表中取出其中一个节点。
ixmlNode getChildNodes (IXML Node* nodeptr )
取出nodeptr的子节点,组成一个节点链表,调用ixmlNodeList_item(child_node_list,1),可以得到:
IXML_Node *ixmlNode_getFirstChild(IXML_Node *nodeptr)
取出一个节点的第一个子节点。
const DOMString ixmlNode_getNodeValue(IXML_Node *nodeptr)
取出一个节点的值。
void ixmlNodeList_free(IXML_NodeList *nList)
释放节点列表空间。
void ixmlDocument_free(IXML_Document *doc)
释放DOC空间
2.举例
对于之前那个XML,如何读取其中的ServiceId值呢?(假设XML为”./web/device_desc.xml”)
IXML_Document *doc_desc;
UpnpDownloadXmlDoc(”./web/device_desc.xml”,&doc_desc);
IXML_NodeList *node_list=ixmlDocument_getElementsByTagName(doc_desc,”ServiceId”);
IXML_Node *node= ixmlNodeList_item(node_list,0);
node= ixmlNode_getFirstChild(node);
char *service_id=strdup(ixmlNode_getNodeValue(node));
ixmlNodeList_free (node_list);
ixmlDocument_free(doc_desc);
….
free(service_id);
三、upnp设备的编写
一、设备的初始化
1.初始化SDK
UpnpInit( ip_address, port );
2.注册虚拟目录:
char* web_dir_path="./web";
UpnpSetWebServerRootDir( web_dir_path );
3.注册根设备
UpnpRegisterRootDevice( desc_doc_path, MyDeviceCallbackEventHandler,
&device_handle, &device_handle );
4.初始化服务和状态(这部分自己完成,非SDK里面的函数):
SetupServiceAndVarible(desc_doc_path);
5.广播设备上线消息:
unsigned int default_advr_expire=100;
UpnpSendAdvertisement( device_handle, default_advr_expire);
6.阻塞主线程,等待设备退出信号,如果退出,调用:
UpnpFinish();
二、处理设备请求
设备广播之后就可以处理其他设备发送过来的请求了,请求是异步和并发的,所以要加锁。一个请求到来就会调用UpnpRegisterRootDevice中注册的回调函数(上面的MyDeviceCallbackEventHandler),定义如下:
int MyDeviceCallbackEventHandler(Upnp_EventType EventType, void *Event, void *Cookie)
EventType表示请求的类型,作为一个设备而言,它只需要处理三种请求:
UPNP_EVENT_SUBSCRIPTION_REQUEST:订阅请求
UPNP_CONTROL_GET_VAR_REQUEST: 变量请求
UPNP_CONTROL_ACTION_REQUEST: 动作请求
Event保存请求信息的结构体
设备处理订阅请求:
1.将Event转换为订阅请求类型:
(struct Upnp_Subscription_Request *)Event
2.从请求结构体中获取udn,service_id,sid
const char *l_serviceId = NULL;
const char *l_udn = NULL;
const char *l_sid = NULL;
l_serviceId = sr_event->ServiceId;
l_udn = sr_event->UDN;
l_sid = sr_event->Sid;
3.跟据service_id和udn查找设备提供的服务列表,如果有匹配项,那么接受订阅:
UpnpAcceptSubscription(device_handle,l_udn,l_serviceId,
(const char**)g_dev_service_list[i].VariableName,
(const char**)g_dev_service_list[i].VariableStrVal,
g_dev_service_list[i].VariableCount,l_sid);
处理动作请求:
1.将Event转换为动作请求类型:
(struct Upnp_Action_Request *)Event
2.从请求结构体中获取udn,service_id,action_name:
const char *dev_udn = NULL;
const char *service_id = NULL;
const char *action_name = NULL;
dev_udn = ca_event->DevUDN;
service_id = ca_event->ServiceID;
action_name = ca_event->ActionName;
3.跟据udn、service_id和action_name查找对应的action函数,如果找到了就调用这个action函数,action函数定义如下:
typedef int (*upnp_action) (IXML_Document *request, IXML_Document **out, char **errorString);
IXML_Document action_result;
char *error_string;
ret_code=g_dev_service_list[i].actions[j](
ca_event->ActionResult,
&ca_event->ActionResult,
&error_string,
(void*)&g_dev_service_list[i]
);
if(ret_code == UPNP_E_SUCCESS)
ca_event->ErrCode=UPNP_E_SUCCESS;
如果没有发现匹配的action,那么返回401的错误代码:
ca_event->ActionResult=NULL;
strcpy(ca_event->ErrStr, "Invalid Action" );
ca_event->ErrCode=401;
在action函数中,通常可能改变了服务状态变量的值,这时候要调用通知函数UpnpNotify:
UpnpNotify( device_handle,
pservice->UDN,
pservice->ServiceId,
( const char ** )&pservice->VariableName[VAR_INDEX_POWER],
( const char ** )&pservice->VariableStrVal[VAR_INDEX_POWER], 1);
action函数中处理完和设备的相关数据后,调用UpnpAddToActionResponse设置返回结果:
if( UpnpAddToActionResponse( out,pservice->ActionNames[ACT_INDEX_POWERON],
pservice->ServiceType,
pservice->VariableName[VAR_INDEX_POWER],
pservice->VariableStrVal[VAR_INDEX_POWER]) != UPNP_E_SUCCESS ) {
*out= NULL;
*errorString = "Internal Error";
return UPNP_E_INTERNAL_ERROR;
}
处理变量请求:
1. 将Event转换为变量请求类型:
(struct Upnp_State_Var_Request *)Event
2.获取udn,service_id和var_name:
dev_udn=cgv_event->DevUDN;
service_id=cgv_event->ServiceID;
var_name=cgv_event->StateVarName;
3.在服务列表中查找匹配项,如果找到就将变量值设置到Event中:
for(i=0;i<DEV_SERVICE_COUNT;i++){
if(!strcmp(g_dev_service_list[i].UDN,dev_udn) &&
!strcmp(g_dev_service_list[i].ServiceId,service_id)){
for(j=0;j<g_dev_service_list[i].VariableCount;j++){
if(!strcmp(g_dev_service_list[i].VariableName[j],var_name)){
cgv_event->CurrentVal = ixmlCloneDOMString(
g_dev_service_list[i].VariableStrVal[j]);
break;
}
}
break;
}
}
4.设置好Event中的返回值:
if(i==DEV_SERVICE_COUNT && j==g_dev_service_list[i].VariableCount){
cgv_event->ErrCode=404;
strcpy(cgv_event->ErrStr, "Invalid Variable" );
}else{
cgv_event->ErrCode=UPNP_E_SUCCESS;
}
四、编写UPNP控制点
一、控制点的流程:
1.初始化SDK库:
int UpnpInit(const char *HostIP, unsigned short DestPort)
2.注册控制点:
int UpnpRegisterClient(
Upnp_FunPtr Fun,
const void *Cookie,
UpnpClient_Handle *Hnd)
3.发出搜索:
int UpnpSearchAsync(
UpnpClient_Handle Hnd,
int Mx,
const char *Target_const,
const void *Cookie_const )
4.处理各种事件
5.退出:
UpnpUnRegisterClient(g_ctrl_handle);
UpnpFinish();
二、控制点处理的事件:
1. UPNP_DISCOVERY_SEARCH_RESULT和UPNP_DISCOVERY_ADVERTISEMENT_ALIVE
在调用UpnpSearchAsync发出搜索请求后,设备端收到请求会返回UPNP_DISCOVERY_SEARCH_RESULT,如果超时会得到UPNP_DISCOVERY_SEARCH_TIMEOUT。
设备端上线后会广播一次,这时控制点会收到UPNP_DISCOVERY_ADVERTISEMENT_ALIVE的消息。
收到这两个事件时,需要跟据事件中的URL将设备的XML文档下载过来,然后将设备添加到控制点维护的设备链表中,以供后期使用。
location = event->Location;
err_code = UpnpDownloadXmlDoc(location, &desc_doc);
if (err_code != UPNP_E_SUCCESS) {
dprinterr("Error obtaining device description from %s — error = %d",
location, err_code);
} else {
add_device_to_list(desc_doc, location, event->Expires);
}
if( desc_doc ) {
ixmlDocument_free(desc_doc);
}
2. UPNP_DISCOVERY_ADVERTISEMENT_BYEBYE
当设备下线时会广播此消息,控制点收到后,需要把设备从链表中移除。
int err_code = event->ErrCode;
if (err_code != UPNP_E_SUCCESS) {
dprinterr("Error in Discovery ByeBye Callback — %d", err_code);
}
const char *udn = event->DeviceId;
remove_device(udn);
3. UPNP_CONTROL_ACTION_COMPLETE
当控制点发送了ACTION后,会收到这个消息,它是用来返回ACTION执行的结果,控制点也可以通过这个判断ACTION是否执行成功,是否需要再次发送ACTION。
4. UPNP_CONTROL_GET_VAR_COMPLETE
当控制点发送获取设备状态后,会收到此消息,可以从消息中读取到状态。
5. UPNP_EVENT_RECEIVED
当控制点发送订阅服务,并订阅成功后,如果设备调用Notify,就会收到此消息,主要用于设备通知控制点状态发生变化。
三、控制点发出的请求
1.搜索请求
int UpnpSearchAsync(
UpnpClient_Handle Hnd,
int Mx,//超时时间,单位秒
const char *TTarget_constarget_const,//搜索匹配条件
const void *Cookie_const);
搜索匹配条件可以是以下:
ssdp:all 搜索所有的设备和服务
upnp:rootdevice 只搜索根设备
uuid:device-UUID 搜索特定的设备
urn:schemas-upnp-org:device:deviceType:ver 搜索某一类型的设备
urn:schemas-upnp-org:service:serviceType:ver 搜索某一类型的服务
urn:domain-name:device:deviceType:ver
urn:domain-name:service:serviceType:ver
发出搜索请求后,如果此设备在网络上,就会返回UPNP_DISCOVERY_SEARCH_RESULT,如果不能及时返回,应用程序就会收到UPNP_DISCOVERY_SEARCH_TIMEOUT。
2.动作请求
找到设备以后,可以请求设备执行某项动作,这些动作可以是控制设备开关,也可以是返回设备状态(据说UPNP论坛推荐这样做,而不是使用请求状态变量来获取设备状态)。
在发送动作请求之前,需要创建一个动作:
IXML_Document *UpnpMakeAction(
const char *ActionName,
const char *ServType,
int NumArg,
const char *Arg,…);
int UpnpAddToAction(
IXML_Document **ActionDoc,
const char *ActionName,
const char *ServType,
const char *ArgName,
const char *ArgVal);
创建好动作以后,就可以开始发送动作了:
int UpnpSendActionAsync(
UpnpClient_Handle Hnd,
const char *ActionURL,
const char *ServiceType,
const char *DevUDN,
IXML_Document *Action,
Upnp_FunPtr Fun,
const void *Cookie);
typedef int (*Upnp_FunPtr)(Upnp_EventType EventType,void *Event, void *Cookie);
其中Upnp_FunPtr是回调函数,里面可以得到动作执行的结果。
示例:
if( 0 == param_count ) {
action_node =UpnpMakeAction(actionname,service_type, 0,NULL );
} else {
for( param = 0; param < param_count; param++ ){
if( UpnpAddToAction(&action_node,actionname,service_type,param_name[param],
param_val[param]) != UPNP_E_SUCCESS ) {
dprinterr("ERROR:Trying to add action param");
ithread_mutex_unlock( &g_ctrl_mutex );
return -1;
}
}
}
ret_code = UpnpSendActionAsync(client_handle,ctrl_url, service_type,NULL,action_node,
upnp_ctrl_event_handler,NULL);
3.设备状态请求
有两种方法可以获取到当前设备的状态,一种是使用动作请求,一种是设备状态请求,其中后面一种不推荐使用了。
int UpnpGetServiceVarStatus(
UpnpClient_Handle Hnd,
const char *ActionURL,
const char *VarName,
DOMString *StVarVal);
int UpnpGetServiceVarStatusAsync(
UpnpClient_Handle Hnd,
const char *ActionURL,
const char *VarName,
Upnp_FunPtr Fun,
const void *Cookie);
4.订阅请求
设备状态发生变化时,也可以主动调用Notify函数通知已订阅此状态的控制点。
控制点调用下面这个函数订阅服务的状态:
int UpnpSubscribeAsync(
UpnpClient_Handle Hnd,
const char *PublisherUrl,//event_url
int TimeOut,
Upnp_FunPtr Fun,
const void *Cookie);
取消订阅:
int UpnpUnSubscribe(
UpnpClient_Handle Hnd,
const Upnp_SID SubsId);
五、数据的传输
可以使用HTTP进行数据传输:
1.从服务器获取数据:
UpnpOpenHttpGet
UpnpReadHttpGet
UpnpCloseHttpGet
2.提交文件到服务器:
UpnpOpenHttpPost
UpnpWriteHttpPost
UpnpCloseHttpPost
六、UPNP的标准服务
主要包括下面四个标准服务:
1. Content Directory Service: Enumerates the available content.
2. Connection Manager Service: Determines how the content can be transferred from the UPnP
AV MediaServer to the UPnP AV MediaRenderer devices.
3. AV Transport Service: Controls the flow of the content.
4. Rendering Control Service: Controls how the content is played.