【BLE】CC2541之添加自定义服务

本篇博文最后修改时间:2017年01月06日,11:06。


一、简介

本文以SimpleBLEPeripheral工程为例,介绍如何在工程中添加一个自定义的服务。


二、实验平台

协议栈版本:BLE-CC254x-1.4.0

编译软件:IAR 8.20.2

硬件平台:Smart RF开发板(主芯片CC2541)

手机平台:小米1S

APP:TruthBlue2.7


版权声明

博主:甜甜的大香瓜

声明:喝水不忘挖井人,转载请注明出处。

原文地址:http://blog.csdn.NET/feilusia

联系方式:897503845@qq.com

香瓜BLE之CC2541群:127442605

香瓜BLE之CC2640群:557278427

香瓜BLE之Android群:541462902

香瓜单片机之STM8/STM32群:164311667
甜甜的大香瓜的小店(淘宝店):https://shop217632629.taobao.com/?spm=2013.1.1000126.d21.hd2o8i

四、 实验前提
1、在进行本文步骤前,请先 阅读 以下博文:
1)《CC2541之发现服务与特征值》: http://blog.csdn.net/feilusia/article/details/46909847
2)《CC2541之notify》: http://blog.csdn.net/feilusia/article/details/47020073
3)《CC2541之添加特征值》: http://blog.csdn.net/feilusia/article/details/48235691

2、在进行本文步骤前,请先 实现以下博文:
暂无


五、基础知识

暂无


六、实验步骤

1、编写并添加自定义的服务

1)编写一个GUA_Profile.c(存放在“……\BLE-CC254x-1.4.0\Projects\ble\SimpleBLEPeripheral\Source\GUA”路径下)

//******************************************************************************    
//name:         GUA_profile.c    
//introduce:    香瓜自定义的服务,内含一个可读、可写、可通知的特征值
//author:       甜甜的大香瓜  
//changetime:   2016.2.23
//email:        897503845@qq.com
//****************************************************************************** 
/*********************************************************************
 * INCLUDES
 */
#include "bcomdef.h"
#include "OSAL.h"
#include "linkdb.h"
#include "att.h"
#include "gatt.h"
#include "gatt_uuid.h"
#include "gattservapp.h"
#include "gapbondmgr.h"

#include "GUA_Profile.h"

/*********************************************************************
 * MACROS
 */

/*********************************************************************
 * CONSTANTS
 */

#define SERVAPP_NUM_ATTR_SUPPORTED              5

//属性在属性表中的偏移值
#define ATTRTBL_GUA_CHAR1_IDX                   2
#define ATTRTBL_GUA_CHAR1_CCC_IDX               3
/*********************************************************************
 * TYPEDEFS
 */

/*********************************************************************
 * GLOBAL VARIABLES
 */
// GUA Service UUID: 0xFFE0
CONST uint8 GUAServUUID[ATT_BT_UUID_SIZE] =
{ 
  LO_UINT16(GUAPROFILE_SERV_UUID), HI_UINT16(GUAPROFILE_SERV_UUID)
};

// GUA char1 UUID: 0xFFE1
CONST uint8 GUAChar1UUID[ATT_BT_UUID_SIZE] =
{ 
  LO_UINT16(GUAPROFILE_CHAR1_UUID), HI_UINT16(GUAPROFILE_CHAR1_UUID)
};

/*********************************************************************
 * EXTERNAL VARIABLES
 */

/*********************************************************************
 * EXTERNAL FUNCTIONS
 */

/*********************************************************************
 * LOCAL VARIABLES
 */

static GUAProfileCBs_t *GUAProfile_AppCBs = NULL;

/*********************************************************************
 * Profile Attributes - variables
 */

// GUA Service attribute
static CONST gattAttrType_t GUAProfile_Service = { ATT_BT_UUID_SIZE, GUAServUUID };

// GUA Characteristic 1 Properties
static uint8 GUAProfile_Char1_Props = GATT_PROP_READ | GATT_PROP_WRITE | GATT_PROP_NOTIFY;

// GUA Characteristic 1 Value
static uint8 GUAProfile_Char1[GUAPROFILE_CHAR1_LEN] = {0};

// GUA Characteristic 1 Configs
static gattCharCfg_t GUAProfile_Char1_Config[GATT_MAX_NUM_CONN];

// GUA Characteristic 1 User Description
static uint8 GUAProfile_Char1_UserDesp[10] = "GUA Char1\0";


/*********************************************************************
 * Profile Attributes - Table
 */

static gattAttribute_t GUAProfileAttrTbl[SERVAPP_NUM_ATTR_SUPPORTED] = 
{
  // GUA Service
  { 
    { ATT_BT_UUID_SIZE, primaryServiceUUID }, /* type */
    GATT_PERMIT_READ,                         /* permissions */
    0,                                        /* handle */
    (uint8 *)&GUAProfile_Service                       /* pValue */
  },

    // GUA Characteristic 1 Declaration
    { 
      { ATT_BT_UUID_SIZE, characterUUID },
      GATT_PERMIT_READ, 
      0,
      &GUAProfile_Char1_Props 
    },

      // GUA Characteristic 1 Value
      { 
        { ATT_BT_UUID_SIZE, GUAChar1UUID },
        GATT_PERMIT_READ | GATT_PERMIT_WRITE, 
        0, 
        GUAProfile_Char1
      },

      // GUA Characteristic 1 configuration
      { 
        { ATT_BT_UUID_SIZE, clientCharCfgUUID },
        GATT_PERMIT_READ | GATT_PERMIT_WRITE, 
        0, 
        (uint8 *)GUAProfile_Char1_Config 
      },

      // GUA Characteristic 1 User Description
      { 
        { ATT_BT_UUID_SIZE, charUserDescUUID },
        GATT_PERMIT_READ, 
        0, 
        GUAProfile_Char1_UserDesp 
      },      
};


/*********************************************************************
 * LOCAL FUNCTIONS
 */
static uint8 GUAProfile_ReadAttrCB( uint16 connHandle, gattAttribute_t *pAttr, 
                            uint8 *pValue, uint8 *pLen, uint16 offset, uint8 maxLen );
static bStatus_t GUAProfile_WriteAttrCB( uint16 connHandle, gattAttribute_t *pAttr,
                                 uint8 *pValue, uint8 len, uint16 offset );

static void GUAProfile_HandleConnStatusCB( uint16 connHandle, uint8 changeType );

/*********************************************************************
 * PROFILE CALLBACKS
 */
// GUAProfile Service Callbacks
CONST gattServiceCBs_t GUAProfileCBs =
{
  GUAProfile_ReadAttrCB,        // Read callback function pointer
  GUAProfile_WriteAttrCB,       // Write callback function pointer
  NULL                          // Authorization callback function pointer
};

/*********************************************************************
 * PUBLIC FUNCTIONS
 */

/*********************************************************************
 * @fn      GUAProfile_AddService
 *
 * @brief   Initializes the GUA service by registering
 *          GATT attributes with the GATT server.
 *
 * @param   services - services to add. This is a bit map and can
 *                     contain more than one service.
 *
 * @return  Success or Failure
 */
bStatus_t GUAProfile_AddService( uint32 services )
{
  uint8 status = SUCCESS;

  // Initialize Client Characteristic Configuration attributes
  GATTServApp_InitCharCfg( INVALID_CONNHANDLE, GUAProfile_Char1_Config );

  // Register with Link DB to receive link status change callback
  VOID linkDB_Register( GUAProfile_HandleConnStatusCB );  
  
  
  if ( services & GUAPROFILE_SERVICE )
  {
    // Register GATT attribute list and CBs with GATT Server App
    status = GATTServApp_RegisterService( GUAProfileAttrTbl, 
                                          GATT_NUM_ATTRS( GUAProfileAttrTbl ),
                                          &GUAProfileCBs );
  }

  return ( status );
}

/*********************************************************************
 * @fn      GUAProfile_RegisterAppCBs
 *
 * @brief   Registers the application callback function. Only call 
 *          this function once.
 *
 * @param   callbacks - pointer to application callbacks.
 *
 * @return  SUCCESS or bleAlreadyInRequestedMode
 */
bStatus_t GUAProfile_RegisterAppCBs( GUAProfileCBs_t *appCallbacks )
{
  if ( appCallbacks )
  {
    GUAProfile_AppCBs = appCallbacks;
    
    return ( SUCCESS );
  }
  else
  {
    return ( bleAlreadyInRequestedMode );
  }
}

/*********************************************************************
 * @fn      GUAProfile_SetParameter
 *
 * @brief   Set a GUA Profile parameter.
 *
 * @param   param - Profile parameter ID
 * @param   len - length of data to right
 * @param   pValue - pointer to data to write.  This is dependent on
 *          the parameter ID and WILL be cast to the appropriate 
 *          data type (example: data type of uint16 will be cast to 
 *          uint16 pointer).
 *
 * @return  bStatus_t
 */
bStatus_t GUAProfile_SetParameter( uint8 param, uint8 len, void *pValue )
{
  bStatus_t ret = SUCCESS;
  switch ( param )
  {
    case GUAPROFILE_CHAR1:
      if ( len == GUAPROFILE_CHAR1_LEN ) 
      {
        VOID osal_memcpy( GUAProfile_Char1, pValue, GUAPROFILE_CHAR1_LEN );
      }
      else
      {
        ret = bleInvalidRange;
      }
      break;
    
    default:
      ret = INVALIDPARAMETER;
      break;
  }
  
  return ( ret );
}

/*********************************************************************
 * @fn      GUAProfile_GetParameter
 *
 * @brief   Get a GUA Profile parameter.
 *
 * @param   param - Profile parameter ID
 * @param   pValue - pointer to data to put.  This is dependent on
 *          the parameter ID and WILL be cast to the appropriate 
 *          data type (example: data type of uint16 will be cast to 
 *          uint16 pointer).
 *
 * @return  bStatus_t
 */
bStatus_t GUAProfile_GetParameter( uint8 param, void *pValue )
{
  bStatus_t ret = SUCCESS;
  switch ( param )
  {
    case GUAPROFILE_CHAR1:
      VOID osal_memcpy( pValue, GUAProfile_Char1, GUAPROFILE_CHAR1_LEN );
      break;
      
    default:
      ret = INVALIDPARAMETER;
      break;
  }
  
  return ( ret );
}

/*********************************************************************
 * @fn          GUAProfile_ReadAttrCB
 *
 * @brief       Read an attribute.
 *
 * @param       connHandle - connection message was received on
 * @param       pAttr - pointer to attribute
 * @param       pValue - pointer to data to be read
 * @param       pLen - length of data to be read
 * @param       offset - offset of the first octet to be read
 * @param       maxLen - maximum length of data to be read
 *
 * @return      Success or Failure
 */
static uint8 GUAProfile_ReadAttrCB( uint16 connHandle, gattAttribute_t *pAttr, 
                            uint8 *pValue, uint8 *pLen, uint16 offset, uint8 maxLen )
{
  bStatus_t status = SUCCESS;
 
  // If attribute permissions require authorization to read, return error
  if ( gattPermitAuthorRead( pAttr->permissions ) )
  {
    // Insufficient authorization
    return ( ATT_ERR_INSUFFICIENT_AUTHOR );
  }
  
  // Make sure it's not a blob operation (no attributes in the profile are long
  if ( offset > 0 )
  {
    return ( ATT_ERR_ATTR_NOT_LONG );
  }
 
  if ( pAttr->type.len == ATT_BT_UUID_SIZE )
  {
    // 16-bit UUID
    uint16 uuid = BUILD_UINT16( pAttr->type.uuid[0], pAttr->type.uuid[1]);
    switch ( uuid )
    {
      // No need for "GATT_SERVICE_UUID" or "GATT_CLIENT_CHAR_CFG_UUID" cases;
      // gattserverapp handles this type for reads

      // GUA characteristic does not have read permissions, but because it
      //   can be sent as a notification, it must be included here
      case GUAPROFILE_CHAR1_UUID:
        *pLen = GUAPROFILE_CHAR1_LEN;
        VOID osal_memcpy( pValue, pAttr->pValue, GUAPROFILE_CHAR1_LEN );
        break;

      default:
        // Should never get here!
        *pLen = 0;
        status = ATT_ERR_ATTR_NOT_FOUND;
        break;
    }
  }
  else
  {
    // 128-bit UUID
    *pLen = 0;
    status = ATT_ERR_INVALID_HANDLE;
  }

  return ( status );
}

/*********************************************************************
 * @fn      GUAProfile_WriteAttrCB
 *
 * @brief   Validate attribute data prior to a write operation
 *
 * @param   connHandle - connection message was received on
 * @param   pAttr - pointer to attribute
 * @param   pValue - pointer to data to be written
 * @param   len - length of data
 * @param   offset - offset of the first octet to be written
 *
 * @return  Success or Failure
 */
static bStatus_t GUAProfile_WriteAttrCB( uint16 connHandle, gattAttribute_t *pAttr,
                                 uint8 *pValue, uint8 len, uint16 offset )
{
  bStatus_t status = SUCCESS;
  uint8 notifyApp = 0xFF;
  
  // If attribute permissions require authorization to write, return error
  if ( gattPermitAuthorWrite( pAttr->permissions ) )
  {
    // Insufficient authorization
    return ( ATT_ERR_INSUFFICIENT_AUTHOR );
  }
  
  if ( pAttr->type.len == ATT_BT_UUID_SIZE )
  {
    // 16-bit UUID
    uint16 uuid = BUILD_UINT16( pAttr->type.uuid[0], pAttr->type.uuid[1]);
    switch ( uuid )
    {
      case GUAPROFILE_CHAR1_UUID:  

        if ( offset == 0 )  
        {  
          if ( len != GUAPROFILE_CHAR1_LEN )  
          {  
            status = ATT_ERR_INVALID_VALUE_SIZE;  
          }  
        }  
        else  
        {  
          status = ATT_ERR_ATTR_NOT_LONG;  
        }  
          
        //将接收到的数据写进特征值中,并且置标志位 
        if ( status == SUCCESS )  
        {  
          VOID osal_memcpy( pAttr->pValue, pValue, GUAPROFILE_CHAR1_LEN );  
          notifyApp = GUAPROFILE_CHAR1;  
        }  
             
      break;  
      
      case GATT_CLIENT_CHAR_CFG_UUID:
        //char1通道,则打开notify开关
        if ( pAttr->handle == GUAProfileAttrTbl[ATTRTBL_GUA_CHAR1_CCC_IDX].handle )//GUA CHAR1 NOTIFY  
        {  
          status = GATTServApp_ProcessCCCWriteReq( connHandle, pAttr, pValue, len,  
                                                   offset, GATT_CLIENT_CFG_NOTIFY );  
        }             
        else  
        {  
          status = ATT_ERR_INVALID_HANDLE;  
        }  
        break;
       
      default:
        status = ATT_ERR_ATTR_NOT_FOUND;
        break;
    }
  }
  else
  {
    // 128-bit UUID
    status = ATT_ERR_INVALID_HANDLE;
  }

  // If a charactersitic value changed then callback function to notify application of change
  if ( (notifyApp != 0xFF ) && GUAProfile_AppCBs && GUAProfile_AppCBs->pfnGUAProfileChange )
  {
    GUAProfile_AppCBs->pfnGUAProfileChange( notifyApp );  
  } 
  
  return ( status );
}

/*********************************************************************
 * @fn          GUAProfile_HandleConnStatusCB
 *
 * @brief       GUA Profile link status change handler function.
 *
 * @param       connHandle - connection handle
 * @param       changeType - type of change
 *
 * @return      none
 */
static void GUAProfile_HandleConnStatusCB( uint16 connHandle, uint8 changeType )
{ 
  // Make sure this is not loopback connection
  if ( connHandle != LOOPBACK_CONNHANDLE )
  {
    // Reset Client Char Config if connection has dropped
    if ( ( changeType == LINKDB_STATUS_UPDATE_REMOVED )      ||
         ( ( changeType == LINKDB_STATUS_UPDATE_STATEFLAGS ) && 
           ( !linkDB_Up( connHandle ) ) ) )
    { 
      GATTServApp_InitCharCfg( connHandle, GUAProfile_Char1_Config );
    }
  }
}


//******************************************************************************    
//name:         GUAprofile_Notify    
//introduce:    notify发送函数  
//parameter:    param:特征值通道参数
//              connHandle:连接句柄    
//              pValue:要通知的数据,范围为0~SIMPLEPROFILE_CHAR6,最多20个字节    
//              len:要通知的数据的长度    
//return:       none    
//******************************************************************************   
void GUAprofile_Notify( uint8 param, uint16 connHandle, uint8 *pValue, uint8 len)  
{  
  attHandleValueNoti_t  noti;  
  uint16 value;  
  
  switch ( param )
  {
    case GUAPROFILE_CHAR1:      
      value  = GATTServApp_ReadCharCfg( connHandle, GUAProfile_Char1_Config ); //读出CCC的值  
      
      if ( value & GATT_CLIENT_CFG_NOTIFY )                                     //判断是否打开通知开关,打开了则发送数据  
      {  
        noti.handle = GUAProfileAttrTbl[ATTRTBL_GUA_CHAR1_IDX].handle;  
        noti.len = len;  
        osal_memcpy( noti.value, pValue, len);                                  //数据  
        GATT_Notification( connHandle, &noti, FALSE );  
      }  
      break;

    default:
      break;
  }      
}  

2)写一个头文件GUA_Profile.h(存放在“……\BLE-CC254x-1.4.0\Projects\ble\SimpleBLEPeripheral\Source\GUA”路径下)

//******************************************************************************    
//name:         GUA_profile.h    
//introduce:    香瓜自定义的服务,内含一个可读、可写、可通知的特征值
//author:       甜甜的大香瓜  
//changetime:   2016.2.23
//email:        897503845@qq.com
//****************************************************************************** 
#ifndef GUA_PROFILE_H
#define GUA_PROFILE_H

#ifdef __cplusplus
extern "C"
{
#endif

/*********************************************************************
 * INCLUDES
 */

/*********************************************************************
 * CONSTANTS
 */

// Profile Parameters
#define GUAPROFILE_CHAR1                       0  // RW uint8 - Profile GUA Characteristic 1 value
 
// GUA Service UUID
#define GUAPROFILE_SERV_UUID                   0xFFE0
    
// GUA CHAR1 UUID
#define GUAPROFILE_CHAR1_UUID                  0xFFE1

// GUA Profile Services bit fields
#define GUAPROFILE_SERVICE                     0x00000001

// Length of GUA Characteristic 1 in bytes
#define GUAPROFILE_CHAR1_LEN                   20  
/*********************************************************************
 * TYPEDEFS
 */

  
  
/*********************************************************************
 * MACROS
 */

/*********************************************************************
 * Profile Callbacks
 */
// Callback when a characteristic value has changed
typedef void (*GUAProfileChange_t)( uint8 paramID );

typedef struct
{
  GUAProfileChange_t        pfnGUAProfileChange;  // Called when characteristic value changes
} GUAProfileCBs_t;

/*********************************************************************
 * API FUNCTIONS 
 */

/*
 * GUAProfile_AddService- Initializes the GUA service by registering
 *          GATT attributes with the GATT server.
 *
 * @param   services - services to add. This is a bit map and can
 *                     contain more than one service.
 */

extern bStatus_t GUAProfile_AddService( uint32 services );
  
/*
 * GUAProfile_RegisterAppCBs - Registers the application callback function.
 *                    Only call this function once.
 *
 *    appCallbacks - pointer to application callbacks.
 */
extern bStatus_t GUAProfile_RegisterAppCBs( GUAProfileCBs_t *appCallbacks );

/*
 * GUAProfile_SetParameter - Set a Simple Key Profile parameter.
 *
 *    param - Profile parameter ID
 *    len - length of data to right
 *    pValue - pointer to data to write.  This is dependent on
 *          the parameter ID and WILL be cast to the appropriate 
 *          data type (example: data type of uint16 will be cast to 
 *          uint16 pointer).
 */
extern bStatus_t GUAProfile_SetParameter( uint8 param, uint8 len, void *pValue );
  
/*
 * GUA_GetParameter - Get a Simple Key Profile parameter.
 *
 *    param - Profile parameter ID
 *    pValue - pointer to data to write.  This is dependent on
 *          the parameter ID and WILL be cast to the appropriate 
 *          data type (example: data type of uint16 will be cast to 
 *          uint16 pointer).
 */
extern bStatus_t GUAProfile_GetParameter( uint8 param, void *pValue );

//******************************************************************************    
//name:         GUAprofile_Notify    
//introduce:    notify发送函数  
//parameter:    param:特征值通道参数
//              connHandle:连接句柄    
//              pValue:要通知的数据,范围为0~SIMPLEPROFILE_CHAR6,最多20个字节    
//              len:要通知的数据的长度    
//return:       none    
//******************************************************************************  
extern void GUAprofile_Notify( uint8 param, uint16 connHandle, uint8 *pValue, uint8 len);
/*********************************************************************
*********************************************************************/

#ifdef __cplusplus
}
#endif

#endif /* GUA_PROFILE_H */

3) 在工程的PROFILE分类之中,添加“GUA_Profile.c、GUA_Profile.h”



4)在IAR设置中添加按键驱动源文件路径

$PROJ_DIR$\..\..\SimpleBLEPeripheral\Source\GUA

2、在应用层中添加服务相关的代码

1)添加GUA_Profile头文件(SimpleBLEPeripheral.c中)

//香瓜
#include "GUA_Profile.h"   
//香瓜

2)服务初始化(SimpleBLEPeripheral.c的SimpleBLEPeripheral_Init中)

//香瓜
  //增加服务
  GUAProfile_AddService(GATT_ALL_SERVICES);

  //初始化特征值
  uint8 GUAProfile_Char1Value[GUAPROFILE_CHAR1_LEN] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
  GUAProfile_SetParameter( GUAPROFILE_CHAR1, GUAPROFILE_CHAR1_LEN, &GUAProfile_Char1Value );

  //添加回调函数
  VOID GUAProfile_RegisterAppCBs( &simpleBLEPeripheral_GUAProfileCBs );    
//香瓜

3)定义服务的回调函数(SimpleBLEPeripheral.c中)

/*********************************************************************
 * @fn      GUAProfileChangeCB
 *
 * @brief   Callback from GUAProfile indicating a value change
 *
 * @param   paramID - parameter ID of the value that was changed.
 *
 * @return  none
 */
static void GUAProfileChangeCB( uint8 paramID )
{
  switch( paramID )
  {
    case GUAPROFILE_CHAR1:
   
      uint16 notify_Handle;   
      uint8 bBuf[20] = {0}; 
      uint8 *p = bBuf;   
        
      GAPRole_GetParameter( GAPROLE_CONNHANDLE, &notify_Handle);        //获取Connection Handle   
        
      for(uint8 i = 0; i < 20; i++)                                     //写一个20字节的测试缓冲区的数据  
      {  
        *(p+i) = i;  
      }  
    
      GUAprofile_Notify(GUAPROFILE_CHAR1, notify_Handle, p, 20);      

      break;

    default:
      // should not reach here!
      break;
  }
}
里面添加了测试代码,一旦接收到数据,就把0~19发给主机。


4)声明服务的回调函数(SimpleBLEPeripheral.c中)

//香瓜
static void GUAProfileChangeCB( uint8 paramID );
//香瓜

5)注册回调函数

//香瓜
// GUA Profile Callbacks
static GUAProfileCBs_t simpleBLEPeripheral_GUAProfileCBs =
{
  GUAProfileChangeCB    // Charactersitic value change callback
};
//香瓜

七、注意事项

手机可能缓存了之前的代码(在更新过CC2541的代码之后,都需要清除手机端的缓存!!!),因此要清除缓存,清除缓存的方法如下:

方法一:关闭app、关闭蓝牙总开关、打开蓝牙总开关、打开app。
方法二:手机重启。


八、实验结果

1、所有服务的界面


添加好上面的代码之后,用手机app即可查看到新增的服务ffe0,与该服务下的特征值ffe1。


2、数据收发


在app上任意写20个字节发往从机,从机会回0~19。

注:图中显示的格式是HEX。



  • 7
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值