零、SDK的安装
upnp的概念就不理会了,网上很多,这里偏向于具体编程。
SDK使用upnp1.6.17版本,这是一个linux下的开源版本,目前仍然在维护,下载地址:
http://pupnp.sourceforge.net/
安装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:
节点是XML中一个很重要的概念,对于理解后面操作XML有帮助,下面这些都是节点:
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 if(!strcmp(g_dev_service_list[i].UDN,dev_udn) && !strcmp(g_dev_service_list[i].ServiceId,service_id)){ for(j=0;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.