UPNP编程

丁阎宝
2023-12-01

零、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目录下就可以找到includelib两个目录了,里面就是头文件和库,在upnp/sample目录下是示例程序。

交叉编译目前没有去理会,先学习下X86下面UPNP的编程。

另外可以在网上下载intel upnp tools来对自己编写的设备进行测试。

一、名词解释和XML文档

UDNuuid,设备唯一名

SID:订阅标识,唯一

ServiceId:服务ID号,唯一

URIUniversal Resource Identifier

UPNP编程分为设备端和客户端(控制点),设备端采用XML来提供自己的信息,主要包括自身描述XML文档和动作状态文档。

自身描述XML文档格式:

见文档。

动作状态XML文档格式:

见文档。

这些XML的编写,除了自己按照格式填之外,还可以使用Intel upnp tools中的DeviceBuildServiceAuthor生成。我测试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_idudn查找设备提供的服务列表,如果有匹配项,那么接受订阅:

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.跟据udnservice_idaction_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_idvar_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.

 类似资料: