• 利用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

  • 相关阅读:
    Android UI开发 popupwindow介绍以及代码实例
    前端之Android入门(5) – MVC模式(下)
    前端之Android入门(4) – MVC模式(中)
    前端之Android入门(3) – MVC模式(上)
    前端之Android入门(2) – 程序目录及UI简介
    前端之Android入门(1) – 环境配置
    android之SQLite数据库应用(二)
    android之SQLite数据库应用(一)
    android 裁剪图片大小 控制图片尺寸
    Android应用盈利广告平台的嵌入方法详解
  • 原文地址:https://www.cnblogs.com/hongyin163/p/1472327.html
Copyright © 2020-2023  润新知