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機器人的服務來讓大家了解一個通用的、一個很有用的架構,架構圖如下所示:
圖1-PC和機器人遠程連接
圖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