利用Microsoft Robotics Studio远程控制机器人

Microsoft Robotics Studio可以使你在pc上创建程序来远程控制机器人,当然我们知道,微软的机器人软件开发平台是架构在.NET和.NET CF平台下的,如果你的机器人自身安装了.NET 或.NET CF的话,那么就机器人就可以脱机跑了,本文所说的方法是针自身没能够安装.NET和.NET CF平台的机器人的哦。

这个实例主要讲解怎样为你的远程连接(有线或无线)机器人实现一个PC端的控制接口,实际上我们要实现一个负责和机器人通信的服务,为了使这个服务更加通用,这个服务需要实现Microsoft Robotics Studio中所定义的通用协议,例如为Motor,Bumper,Contract,sonar等传感器,Microsoft Robotics Studio为这些传感器定义了统一通信协议,包括消息类型,消息体等,这些协议在 Robotics Common找到,这些协议使得我们隐藏机器人的细节,实现了这些协议,就可以在VPL中使用一致的操作方式使用这些模块了。

这个实例主要有以下几个方面:

 

  • 在机器人一端创建一个远程通信的接口
  • 在PC端创建一个和机器人硬件交互的接口
  • 使用Brick Service
  • 实现一个通用的服务

 

准备:

硬件:这个实例目的是帮助msrs不支持的硬件开发服务,你可能会发现,使用下面的平台会来学这个实例比较有帮助的。

  • LEGO MINDSTORMS NXT
  • fischertechnik
  • iRobot Create

硬件制造商通常会为自己的平台提服务供支持,在为这些硬件写服务的时候可以看下官方网站或论坛,可有这样的服务已经有人写好了哦。

软件:这个实例是为使用Visual C#的开发人员提供的,你可以使用下面的开发工具:

  • Microsoft Visual C# Expss Edition
  • Microsoft Visual Studio Standard, Professional, or Team Edition.

开始

这个实例由C#语言编写,你可以在下面的MSRS目录中找到这个实例的项目文件。

Samples/RoboticsTutorials/Tutorial6/CSharp

 

概述

这个实例通过分析LEGO NET机器人的服务来让大家了解一个通用的、一个很有用的架构,架构图如下所示:

机器人,Robot
图1-PC和机器人远程连接

LEGO NXT机器人基础架构图
图2-LegoNxt机器人的服务架构

第一步:在机器人上为远程通信开发通信接口

你的机器人需要为外界提供接口,通过这个接口我们可以获取机器人的传感器和电机的信息,接口相应的程序必须运行在机器人自身系统上,比如单片机、arm等,如果机器人已经提供了远程通信的接口(比如iRobot Create•,LegoNXT等机器人已经实现了这样的接口),那么你可以跳过此步了哦。

假如你的机器人不包括一个通信接口,你需要用自己去开发这样的接口,通过机器人所支持的开发工具开发程序,这些程序可以监视传感器的改变,并且向一个连接的PC端发送回消息,它应该也可以很好的处理所接收的马达消息请求,这些程序应该是一个循环类的程序。

第二步:

现在把精力集中在运行在PC端的代码上,这些代码和远程的机器人接口通信,代码所实现的服务通过其他的协助服务或C++/CLI库来实现,这个实例中的Brick Service完全负责和机器通信,这个Brick Service可以认为是机器人在mrds平台的一个抽象实体,所有和机器人的交互将由Brick Service实现,mrds通过Brick Service来控制机器人,Brick Service的状态应该包含最新的马达和传感器信息。

LEGO NXT机器人使用一个蓝牙接口,当连接蓝牙后,它会呈现为PC的一个串口提供给用户使用,下面的代码段实现了如何读写串口,这一步最重要的两个部分是:1、确保有权限使用这个串口;2、在合适的位置处理从串口输入的数据。

如何设置串口:

 

SerialPort serialPort  =   new  System.IO.Ports.SerialPort();
void  Open( int  comPort,  int  baudRate)
{
serialPort 
=   new  SerialPort( " COM "   +  comPort.ToString(), baudRate);
serialPort.Encoding 
=  Encoding.Default;
serialPort.Parity 
=  Parity.None;
serialPort.DataBits 
=   8 ;
serialPort.StopBits 
=  StopBits.One;
serialPort.DataReceived 
+=   new  SerialDataReceivedEventHandler(serialPort_DataReceived);
serialPort.Open();
}
// Send data that your robot understands
void  SendData( byte [] buffer)
{
serialPort.Write(buffer, 
0 , buffer.Length);
}

 

假如你在用蓝牙,你可能需要添加一个header,它包括的消息(message)的长度。当从COM口接收到数据后,你应该向服务的内部端口(internal port)提交数据,如果想更新服务的状态,你要确保消息的处理方法是独占使用这个服务的状态的,也就是要获得服务状态的锁,这样才可以改变服务的状态。

关于 内部端口(internal port)、服务(Service)、消息(Message)等名词的定义请查看相关资料 。

 

 1    void  serialPort_DataReceived( object  sender, SerialDataReceivedEventArgs e)
 2  {
 3  //
 4  // Do not modify your state yet
 5  _myRobotInboundPort.Post(sensorMsg);
 6  }
 7  //
 8  protected   override   void  Start()
 9  {
10  Interleave mainInterleave  =  ActivateDsspOperationHandlers();
11  mainInterleave.CombineWith( new  Interleave(
12  new  TeardownReceiverGroup(),
13  new  ExclusiveReceiverGroup(
14  Arbiter.ReceiveWithIterator( true , _myRobotInboundPort, MyRobotSensorMessageHandler)
15  ),
16  new  ConcurrentReceiverGroup()
17  ));
18  }
19  private  IEnumerator MyRobotSensorMessageHandler(SensorNotification sensorMessage)
20  {
21  // update state here
22  _state.sensor  =  sensorMessage.sensor;
23  //
24  }
25  //
26 

 

第三步:使用Brick Service

Brick Service 负责处理对机器人的访问,它把执行请求发送给机器人并且将机器人的传感器信息发送回订阅Brick Service的服务。

在前面的两个实例Service Tutorial 4 (C#) - Supporting Subscriptions和 Service Tutorial 5 (C#) – Subscribing中我们了解了服务的订阅(subscription)方法,但是这两个例子中描述的服务订阅和这里的并不完全合适,前面实例中的服务订阅会将所有传感器数据返回给订阅者,而下面所说的自定义服务(Custom Subscriptions)只返回传感器数据的一个子集,例如一个订阅了brickService的红外传感器服务并不想获取其他传感器的数据,它只是获得红外传感器的数据,如一个接触(Contract)传感器服务只订阅了碰撞传感器(bumper)的数据,它也不会得到其他传感器的数据,下面就讲解如何是自定义订阅服务。

自定义订阅(Custom Subscriptions)

和一般订阅的实现方式一样,自定义订阅同样使用订阅管理器(subscription manager)处理消息的通知,不同的是,当一个自定义订阅请求发送到被订阅服务,同时也会附带发送一个消息,这个消息用来告诉被订阅服务我们要订阅那些传感器的数据,这个消息是一个列表(List),是一个要订阅的传感器的名称的列表,被订阅服务可以支持这个传感器名称列表的“逻辑或”或者“逻辑与”操作(逻辑或即如何列表里有任一个数据改变,就要发出通知,逻辑与即列表里所有传感器数据发生改变才发出通知。)

下面的代码演示了使用一个逻辑或来订阅服务,也就是说当任何一个过滤字符串匹配后,它都会通知订阅者,假如逻辑与被实现,需要所有的过滤字符串匹配后才会通知订阅者。

首先,添加自定阅操作到类型文件(type file):

 

 

 1  public   class  MyBrickServiceOperations : PortSet
 2  <
 3  DsspDefaultLookup,
 4  DsspDefaultDrop,
 5  Get,
 6  // IMPORTANT: Because SelectiveSubscribe inherits from Subscribe, it must go on top.
 7  SelectiveSubscribe,
 8  Subscribe
 9  >  {}
10  // The standard subscription
11  public   class  Subscribe : Subscribe
12  <
13  SubscribeRequestType,
14  PortSet
15  <
16  subscriberesponsetype
17  >
18  >  {}
19  // The custom subscription
20  public   class  SelectiveSubscribe : Subscribe
21  <
22  MySubscribeRequestType,
23  PortSet
24  <
25  SubscribeResponseType,
26  Fault
27  >
28  >  { }
29  [DataContract]
30  public   class  MySubscribeRequestType : SubscribeRequestType
31  {
32  // The list of sensors to subscribe to
33  [DataMember]
34  public  List Sensors;
35  }
36 

 

现在添加handler到实现文件(implementation file)

  

 1  //  General Subscription
 2  [ServiceHandler(ServiceHandlerBehavior.Exclusive)]
 3  public  IEnumerator SubscribeHandler(Subscribe subscribe)
 4  {
 5  base .SubscribeHelper
 6  (
 7  subMgrPort,
 8  subscribe.Body,
 9  subscribe.ResponsePort
10  );
11  yield   break ;
12  }
13  //  Custom Subscription
14  [ServiceHandler(ServiceHandlerBehavior.Exclusive)]
15  public  IEnumerator SelectiveSubscribeHandler(SelectiveSubscribe subRequest)
16  {
17  submgr.InsertSubscription selectiveSubscription  =   new  submgr.InsertSubscription
18  (
19  new  submgr.InsertSubscriptionMessage
20  (
21  subRequest.Body.Subscriber,
22  subRequest.Body.Expiration,
23  0
24  )
25  );
26  selectiveSubscription.Body.NotificationCount  =  subRequest.Body.NotificationCount;
27  List subscribeFilter  =   new  List();
28  // items in this loop are OR'ed together in the subscription
29  foreach  ( string  s  in  subRequest.Body.Sensors)
30  {
31  LogInfo( " Adding subscription for:  "   +  s.ToUpper());
32  // you can achieve an AND behavior by adding a list of strings in the new QueryType
33  subscribeFilter.Add( new  submgr.QueryType(s.ToUpper()));
34  }
35  selectiveSubscription.Body.QueryList  =  subscribeFilter.ToArray();
36  subMgrPort.Post(selectiveSubscription);
37  yield   return  Arbiter.Choice
38  (
39  selectiveSubscription.ResponsePort,
40  delegate (dssp.SubscribeResponseType response)
41  {
42  subRequest.ResponsePort.Post(response);
43  },
44  delegate (Fault fault)
45  {
46  subRequest.ResponsePort.Post(fault);
47  });
48  yield   break ;
49  }
50  selectiveSubscription.Body.NotificationCount  =  subRequest.Body.NotificationCount;
51  List subscribeFilter  =   new  List();
52  // items in this loop are OR'ed together in the subscription
53  foreach  ( string  s  in  subRequest.Body.Sensors)
54  {
55  LogInfo( " Adding subscription for:  "   +  s.ToUpper());
56  // you can achieve an AND behavior by adding a list of strings in the new QueryType
57  subscribeFilter.Add( new  submgr.QueryType(s.ToUpper()));
58  }
59  selectiveSubscription.Body.QueryList  =  subscribeFilter.ToArray();
60  subMgrPort.Post(selectiveSubscription);
61  yield   return  Arbiter.Choice
62  (
63  selectiveSubscription.ResponsePort,
64  delegate (dssp.SubscribeResponseType response)
65  {
66  subRequest.ResponsePort.Post(response);
67  },
68  delegate (Fault fault)
69  {
70  subRequest.ResponsePort.Post(fault);
71  }
72  );
73  yield   break ;
74  }
75 

 

最后提交自定义订阅到前面定义的传感器通知handler  

 1  private  IEnumerator MyRobotSensorMessageHandler(SensorNotification sensorMessage)
 2  {
 3  // update state here
 4  _state.sensor  =  sensorMessage.sensor;
 5 
 6  // Build notification list
 7  List notify  =   new  List();
 8  notify.Add(sensorMessage.Name.ToUpper());
 9 
10  //  notify general subscribers
11  subMgrPort.Post
12  (
13  new  submgr.Submit(_state, dssp.DsspActions.ReplaceRequest)
14  );
15  //  notify selective subscribers
16  subMgrPort.Post
17  (
18  new  submgr.Submit(_state, dssp.DsspActions.ReplaceRequest, notify.ToArray())
19  );
20  yield   break ;
21  }
22 

 

注意:服务的开发者定义为订阅者定义的传感器名称和行为,一定要和你自己的命名时一致的。

第四步:实现一个通用服务(Generic Services)

现在我们开发一个新服务,这个服务和brik Service交互,从Brick Service获取传感器的数据,这个服务实现了一个通用的协议,这些协议在RoboticsCommon有定义,即MotorTypes.cs和MotorState.cs,一般开发服务,我们会在*type.cs文件中定义服务的协议,我们是不是要把那两个个通用的协议定义文件拷贝到我们的项目中呢?不需要的,这里就会用到备用服务(Alternate Contracts)的功能,一个服务可以包括多个端口(port),如下面所示:

  

1  [ServicePort( " /RobotOne " , AllowMultipleInstances  =   false )]
2  RobotOneOperations _mainPort  =   new  RobotOneOperations();
3  [AlternateServicePort(AlternateContract  =  robot.Contract.Identifier)]
4  robot.RobotOperations _robotServicePort  =   new  robot.RobotOperations();
5 

 

主端口实现自身定义的操作协议,而备用端口借用了其他服务的协议,注意这里和为服务添加Partner不一样,并没有启动robot这个服务,只是借用了这个服务的操作协议而已。下面说下如何使用备用协议。

备用协议(Alternate Contracts)

“翻译的可能不太正确,Alternate在字典里的意思是候选,备用,我的理解是已经定义好的服务协议,这些协议也可以被其他的服务所使用”

一个服务可以实现一个备用协议(Alternate Contracts),允许你的服务表现出出这个协议相应的行为。在这里,我们通过一个比较简单的Motor 服务来演示如何实现一个备用协议,说这个服务简单是因为这个服务不需要订阅brick 服务,它只是发送motor命令。

使用DSSNewService可以生成一个使用备选服务(Alternate Contracts)的项目,这个项目借用了其他的程序集中的操作协议即Contract,你可以用过DssInfo.exe工具找到gerneric motor的contract,你也可以通过运行在这个节点上的Control Panel Service找到这个Contract。

查看Contract的方法,打开DSS Command Prompt命令行工具,输入下面的命令:

DssInfo /o:”D:/RoboticsCommon” /s:Html bin/RoboticsCommon.dll

这行命令会生产一个RoboticsCommon文件夹,里面的内容是HTML文件,打开index.html,你会看到这个RoboticsCommon.dll中所有的服务及服务的介绍,当然也包括我们要找的Contract。

在输出的Contract列表中可以找到 Genneric Motor的信息。

Contract Only: Generic Motor DssContract: http://schemas.microsoft.com/robotics/2006/05/motor.html Namespace: Microsoft.Robotics.Services.Motor

在DssNewService工具中利用这个Contract Identifier可以生成实现了这个Contract的服务,输入如下的命令行:

DssNewService.exe /service:MyRobotMotor /dir:samples/MyRobotMotor /alt:"http://schemas.microsoft.com/robotics/2006/05/motor.html"

然后打开生成的项目,按下面的说明进行相关更改就可以顺利使用了这个项目了。

添加brick Service proxy的引用到项目,并且保证项目中有RoboticsCommon proxy的引用。


图4-添加服务代理的引用

为brick service proxy添加命名空间

 

1  using  brick  =  Robotics.MyBrickService.Proxy;

 

添加brick service为伙伴服务  

1  [Partner( " MyBrickService " ,
2  Contract  =  brick.Contract.Identifier,
3  CreationPolicy  =  PartnerCreationPolicy.UseExistingOrCreate,
4  Optional  =   false )]
5  brick.MyBrickServiceOperations _myBrickPort  =   new  brick.MyBrickServiceOperations();
6 

 

实现 SetMotorPower消息

 1  [ServiceHandler(ServiceHandlerBehavior.Exclusive)]
 2  public  IEnumerator SetMotorPowerHandler(motor.SetMotorPower setMotorPower)
 3  {
 4       // flip direction if necessary
 5       double  revPow  =  setMotorPower.Body.TargetPower;
 6       if  (_state.ReversePolarity)
 7      {
 8          revPow  *=   - 1.0 ;
 9      }
10 
11       // update state
12      _state.CurrentPower  =  revPow;
13 
14       // convert to native units
15       int  power  =  ( int )Math.Round(revPow  *  _state.PowerScalingFactor);
16 
17       // send hardware specific motor data
18      brick.SetMotor motordata  =   new  brick.SetMotor();
19      motordata.PowerSetpoint  =  power;
20 
21       yield   return  Arbiter.Choice(
22          _myBrickPort.SendMotorCommand(motordata),
23           delegate (DefaultUpdateResponseType success)
24          {
25              setMotorPower.ResponsePort.Post(success);
26          },
27           delegate (Fault failure)
28          {
29              setMotorPower.ResponsePort.Post(failure);
30          }
31      );
32 
33       yield   break ;
34  }
35 

 

订阅服务

我们创建的大多数的服务是为了获取传感器的数据为目的的,这就需要这些服务去订阅Birck Service,他们使用上面所实现的自定义订阅来订阅相应的传感器数据,下面我们看一个MyRobotBumper服务,这个服务实现了Robotics Common中的ContactSensorArray服务的Contract,和上面所说Motor Service的创建方式类似。

 

 1  using  bumper  =  Microsoft.Robotics.Services.ContactSensor.Proxy;
 2  using  brick  =  Robotics.MyBrickService.Proxy;
 3  using  submgr  =  Microsoft.Dss.Services.SubscriptionManager;
 4 
 5  private   void  SubscribeToNXT()
 6  {
 7  //  Create a notification port
 8  brick..MyBrickServiceOperations _notificationPort  =   new  brick.MyBrickServiceOperations();
 9  // create a custom subscription request
10  brick.MySubscribeRequestType request  =   new  brick.MySubscribeRequestType();
11  // select only the sensor and ports we want
12  // NOTE: this name must match the names you define in MyBrickService
13  request.Sensors  =   new  List();
14  foreach  (bumper.ContactSensor sensor  in  _state.Sensors)
15  {
16  // Use Identifier as the port number of the sensor
17  request.Sensors.Add( " TOUCH "   +  sensor.Identifier);
18  }
19  // Subscribe to the brick and wait for a response
20  Activate(
21  Arbiter.Choice(_myBrickPort.SelectiveSubscribe(request, _notificationPort),
22  delegate (SubscribeResponseType Rsp)
23  {
24  // update our state with subscription status
25  subscribed  =   true ;
26  LogInfo( " MyRobotBumper subscription success " );
27  // Subscription was successful, start listening for sensor change notifications
28  Activate(
29  Arbiter.Receive
30  ( true , _notificationPort, SensorNotificationHandler)
31  );
32  },
33  delegate (Fault F)
34  {
35  LogError( " MyRobotBumper subscription failed " );
36  })
37  );
38  }
39  private   void  SensorNotificationHandler(brick.Replace notify)
40  {
41  // update state
42  foreach  (bumper.ContactSensor sensor  in  _state.Sensors)
43  {
44  bool  newval  =  notify.Body.SensorPort[sensor.Identifier  -   1 ==   1   ?   true  :  false ;
45  bool  changed  =  (sensor.pssed  !=  newval);
46  sensor.TimeStamp  =  DateTime.Now;
47  sensor.pssed  =  newval;
48  if  (changed)
49  {
50  // notify subscribers on any bumper pssed or unpssed
51  _subMgrPort.Post( new  submgr.Submit(sensor, DsspActions.UpdateRequest));
52  }
53  }
54  }
55 

 

 

扩展状态

前面的模式很容易实现并且在大多数情况下可以正常工作,因为状态和操作是通用的,但是,有时候我们还是想为状态添加一些信息或者添加一些操作类型,一个好的例子就是 sonar as bumper 服务,这个服务使用声波传感器代替一个碰撞传感器,这个服务实现了ContactSensorArray服务的Contract,除非你自己想添加自己状态,你需要这样的做的原因是这个服务需要包括一个距离和阈值的数据信息,这些在Contract服务没有给出。

注意你的状态类是继承自ContactSensorArrayState 。

调整实现文件: 

 1 
 2  using  bumper  =  Microsoft.Robotics.Services.ContactSensor.Proxy;
 3  using  brick  =  Robotics.MyBrickService.Proxy;
 4  using  submgr  =  Microsoft.Dss.Services.SubscriptionManager;
 5  namespace  Robotics.MyRobotSonarAsBumper
 6  {
 7  [Contract(Contract.Identifier)]
 8  [AlternateContract(bumper.Contract.Identifier)]
 9  [PermissionSet(SecurityAction.PermitOnly, Name = " Execution " )]
10  public   class  MyRobotSonarAsBumperService : DsspServiceBase
11  {
12  [InitialStatePartner(Optional  =   true )]
13  private  MyRobotSonarAsBumperState _state;
14  [ServicePort( " /MyRobotSonarAsBumper " , AllowMultipleInstances  =   true )]
15  private  MyRobotSonarAsBumperOperations _mainPort  =   new  MyRobotSonarAsBumperOperations();
16  [AlternateServicePort(
17  " /MyRobotBumper " ,
18  AllowMultipleInstances  =   true ,
19  AlternateContract = bumper.Contract.Identifier
20  )]
21  private  bumper.ContactSensorArrayOperations
22  _bumperPort  =   new  bumper.ContactSensorArrayOperations();
23  [Partner(
24  " MyRobotBrick " ,
25  Contract  =  brick.Contract.Identifier,
26  CreationPolicy  =  PartnerCreationPolicy.UseExistingOrCreate,
27  Optional  =   false
28  )]
29  private  brick.MyBrickServiceOperations _brickPort  =   new  brick.MyBrickServiceOperations();
30  [Partner(
31  " SubMgr " ,
32  Contract  =  submgr.Contract.Identifier,
33  CreationPolicy  =  PartnerCreationPolicy.CreateAlways,
34  Optional  =   false )]
35  private  submgr.SubscriptionManagerPort _subMgrPort  =   new  submgr.SubscriptionManagerPort();
36 
37 

 

注意:主端口(main port)处理主端口的消息,备用端口处理备用端口的消息。

 

现在为备用端口的消息添加消息处理方法:  

 1  //  Listen on the main port for requests and call the appropriate handler.
 2  Interleave mainInterleave  =  ActivateDsspOperationHandlers();
 3  // listen on alternate service port for requests and call the appropriate handler.
 4  mainInterleave.CombineWith( new  Interleave(
 5  new  TeardownReceiverGroup(
 6  Arbiter.Receive(
 7  false ,
 8  _bumperPort,
 9  DefaultDropHandler
10  )
11  ),
12  new  ExclusiveReceiverGroup(
13  Arbiter.ReceiveWithIterator(
14  true ,
15  _bumperPort,
16  ReplaceHandler
17  ),
18  Arbiter.ReceiveWithIterator(
19  true ,
20  _bumperPort,
21  SubscribeHandler
22  ),
23  Arbiter.ReceiveWithIterator(
24  true ,
25  _bumperPort,
26  ReliableSubscribeHandler
27  )
28  ),
29  new  ConcurrentReceiverGroup(
30  Arbiter.ReceiveWithIterator(
31  true ,
32  _bumperPort,
33  GetHandler
34  ),
35  Arbiter.Receive(
36  true ,
37  _bumperPort,
38  DefaultLookupHandler
39  )
40  )
41  ));

 

注意你需要为主端口和备用端口的相同的消息实现不同处理方法,比如Get,Replace,Dubscribe等,Get消息的处理方法如下:

 1  [ServiceHandler(ServiceHandlerBehavior.Concurrent)]
 2  public  IEnumerator MyGetHandler(Get  get )
 3  {
 4     get .ResponsePort.Post(_state);
 5     yield   break ;
 6  }
 7  public  IEnumerator GetHandler(bumper.Get  get )
 8  {
 9     get .ResponsePort.Post(bumper.ContactSensorArrayState)_state.Clone());
10     yield   break ;
11  }

 

注意:在后面的Get消息处理方法中,需要将子类转换为父类(ContactSensorArrayState),因为在将状态对象序列化时,是序列化它实际的对象类型,假如你想获取基类型的序列化对象,但是实际获得的却是子类对象,所以状态类显式实现了Clone()方法进行类型转换,而不是利用隐式转换!!

最后

说实话,俺没怎么接触过硬件,虽然学的是硬件相关的专业,文章是MSDN里的一篇《Robotics Tutorial 6 (C#) - Remotely Connected Robots》,最近学这个东东,英文看完眼睛疼,而且看完英文就好像什么都没记住一样,我想翻译出来会好些吧?虽然本文没有什么思想之类的东西……,语言也不太顺畅,如果你没接触过Microsoft Robotics Studio ,看起了可能比较难,这个博客=》laneser 对Robotics Studio研究的比较深,可惜只是他不再更新了。

噢耶!You Potential!Our Passoin!

 转自:http://www.elooog.cn/post/62.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值