By Mike Dodaro, translated by Marco Minerva from the original English version.
L’interfacccia Gadgeteer.Interfaces.PWMOutput permette l’utilizzo di un servocontrollo per muovere le parti meccaniche di un device, secondo le esigenze delle applicazioni. Questo esempio dimostra l’uso dell’interfaccia Gadgeteer.Interfaces.PWMOutput e di un servomeccanismo per girare la videocamera su un arco di circa 140 gradi. Il valore del Pulse Width Modulation (modulazione di larghezza di impulso, PWM) per questo servomeccanismo è ottenuto attraverso un servizio REST WCF in esecuzione su un server IIS remoto. L’utente può direzionare la camera per scattare fotografie utilizzando qualunque device in grado di effettuare richieste al servizio. L’applicazione, inoltre, effettua chiamate REST di tipo POST per caricare le fotografie scattate online, in modo che sia possibile accedervi da remoto. Un’applicazione client è descritto nel post: Controlling the Servo using a Windows Phone Application.
Il codice per muovere il servomeccanismo è decisamente facile da scrivere. E’ necessaria un’istanza di GT.Interfaces.PWMOutput per controllare la modulazione di larghezza di impulso del servomeccanismo. E’ anche necessario conoscere il range del PWM del meccanismo stesso, informazione solitamente disponibile online sul sito del produttore. I commenti nel codice contengono il range del mio servocontrollo. La modulazione di larghezza del mio servomeccanismo è espressa in millisecondi, ma l’interfaccia PWMOutput utilizza i nanosecondi. I numeri sottostanti sono convertiti in nanosecondi come richiesto dall’interfaccia.
GT.Interfaces.PWMOutput servo; static uint high = 2100000;static uint low = 900000; static uint delta = high - low;
Nel metodo ProgramStarted, inizializziamo gli oggetti necessari per gestire il servomeccanismo e la fotocamera come segue. La classe EthernetConnection è necessaria per accedere al servizio che imposta la posizione del servomeccanismo. L’applicazione interroga il servizio ogni volta che scatta il timer, allo scopo di ottenere la direzione della camera. Dopo che il meccanismo ha orientato la camera come indicato dal valore di PWM, viene scattata una foto.
void ProgramStarted() { ethernet.UseDHCP(); ethernet.NetworkUp += new GTM.Module.NetworkModule.NetworkEventHandler(ethernet_NetworkUp); servo = extender.SetupPWMOutput(GT.Socket.Pin.Nine); button.ButtonPressed += new Button.ButtonEventHandler(button_ButtonPressed); timer = new GT.Timer(3000); timer.Tick += new GT.Timer.TickEventHandler(timer_Tick); camera.PictureCaptured += new Camera.PictureCapturedEventHandler(camera_PictureCaptured); Debug.Print("Program Started"); }
Il codice completo è illustrato di seguito: in particolare, notiamo i metodi GetServoState() e SendPictureData(GT.Picture picture). Questo post è stato aggiornato perché la soluzione precedente non è necessaria per il modulo Ethernet, come spiegato nel post Soluzione temporanea per il corretto funzionamento del modulo Ethernet_J11D.
<Questo post è stato aggiornato per utilizzare le nuove Networking API di .NET Gadgeteer, ovvero HttpHelper.CreateHttpPostRequest e WebClient.GetFromWeb. I nuovi metodi sono più facili da usare e non bloccano il dispatcher durante le richieste GET e POST al server.>
using Microsoft.SPOT; using Gadgeteer.Networking; using GT = Gadgeteer; using GTM = Gadgeteer.Modules; using Gadgeteer.Modules.GHIElectronics; using System.Xml; namespace ServoCamera { public partial class Program { // All Hitec servos require a 3-4V peak to peak square wave pulse. // Pulse duration is from 0.9ms to 2.1ms with 1.5ms as center. // The pulse refreshes at 50Hz (20ms). GT.Interfaces.PWMOutput servo; GT.Timer timer; static uint high = 2100000; static uint low = 900000; static uint delta = high - low; bool takePictureNow = false; //uint stateResponse; void ProgramStarted() { ethernet.UseDHCP(); ethernet.NetworkUp += new GTM.Module.NetworkModule.NetworkEventHandler(ethernet_NetworkUp); servo = extender.SetupPWMOutput(GT.Socket.Pin.Nine); button.ButtonPressed += new Button.ButtonEventHandler(button_ButtonPressed); timer = new GT.Timer(3000); timer.Tick += new GT.Timer.TickEventHandler(timer_Tick); camera.PictureCaptured += new Camera.PictureCapturedEventHandler(camera_PictureCaptured); Debug.Print("Program Started"); } void ethernet_NetworkUp(GTM.Module.NetworkModule sender, GTM.Module.NetworkModule.NetworkState state) { Debug.Print("Network Up."); } void camera_PictureCaptured(Camera sender, GT.Picture picture) { // Send picture to service. SendPictureData(picture); } void timer_Tick(GT.Timer timer) { // Get PWM setting to aim camera. GetServoState(); } void button_ButtonPressed(Button sender, Button.ButtonState state) { if (timer.IsRunning) { timer.Stop(); button.TurnLEDOff(); } else { timer.Start(); button.TurnLEDOn(); } } public void GetServoState() { WebClient.GetFromWeb("http://integral-data.com/ServoCameraService/state").ResponseReceived += new HttpRequest.ResponseHandler(State_ResponseReceived); } void State_ResponseReceived(HttpRequest sender, HttpResponse response) { takePictureNow = false; uint servoState = 0; if (response.StatusCode == "200") { XmlReader reader = XmlReader.Create(response.Stream); while (reader.Read()) { if (reader.Name == "percent") servoState = uint.Parse(reader.ReadElementString()); if (reader.Name == "pictureRequested") takePictureNow = (reader.ReadElementString() == "true"); } uint pulse = low + (delta * servoState / 100); servo.SetPulse(20000000, pulse); if (takePictureNow) { camera.TakePicture(); } } else { Debug.Print(response.StatusCode); } //stateResponse = servoState; } public void SendPictureData(GT.Picture picture) { led.BlinkRepeatedly(GT.Color.White); POSTContent postData = POSTContent.CreateBinaryBasedContent(picture.PictureData); var reqData = HttpHelper.CreateHttpPostRequest("http://integral-data.com/ServoCameraService/data", postData, "image/bmp"); reqData.ResponseReceived += new HttpRequest.ResponseHandler(reqData_ResponseReceived); reqData.SendRequest(); } void reqData_ResponseReceived(HttpRequest sender, HttpResponse response) { if (response.StatusCode != "200") Debug.Print(response.StatusCode); led.TurnOff(); } } }
Il cuore dell’applicazione è il codice che interroga il Web Service per recuperare il numero PWM che controlla il servomeccanismo. Il numero è la percentuale del delta tra i valori massimo e minimo. Tale numero è restituito dal servizio e utilizzato nel metodo SetPulse sull’interfaccia Gadgeteer.Interfaces.PWMOutput.
Per utilizzare il servomeccanismo è necessario collegare il filo di input ad un pin su un socket P che supporta la modulazione di larghezza di impulso (PWM). L’alimentazione per il motore deve provenire da una batteria esterna o un trasformatore che fornisce corrente a 5 volt. La connessione può essere effettuata utilizzando l’Extender Module di GHI Electronics.
uint pulse = low + (delta * percent / 100); servo.SetPulse(20000000, pulse);
Ecco uno schema del circuito del servocomando dal modulo GHI Extender e una foto del cablaggio. Solo il pin 9 e la terra sul pin 10 sono necessari per il circuito PWM.
Web Service REST per ottenere (GET) il PWM ed inviare (POST) le immagini
Il servizio REST può essere implementato utilizzando il WCF REST Web Service Template 4.0. Per maggiori informazioni sull’uso del template, è possibile fare riferimento ai post Web Service REST per registrare dati da un sensore .NET Gadgeteer e Remote Control of .NET Gadgeteer Device via REST Web Service (quest’ultimo in lingua inglese).
Questo è il codice del servizio REST per gestire il servomeccanismo.
[WebInvoke(UriTemplate = "{percent}", Method = "POST")] public ServoDataControl CreateControl(string percent) { if (control == null) { control = new ServoDataControl(); } control.timeSet = DateTime.Now; control.percent = uint.Parse(percent); control.pictureRequested = true; return control; } [WebGet(UriTemplate = "state")] public ServoDataControl State() { if (control == null) { control = new ServoDataControl(); control.timeSet = DateTime.Now; control.percent = (uint)50; control.pictureRequested = false; } return control; }
Il Web service per caricare le immagini online è mostrato di seguito. Implementare la parte di mantenimento dello stato non ha creato problemi, mentre la gestione corretta dell’upload ha richiesto uno sforzo in più. Marco Minerva, forte della sua esperienza in questo campo, mi ha dato una mano fornendo parte del codice in una discussione che potete seguire sul forum di MSDN dedicato a WCF.
Innanzi tutto, vediamo la classe per la salvare e mantenere lo stato:
using System; namespace WcfRestServoCameraService { public class ServoDataControl { public uint percent { get; set; } public DateTime timeSet { get; set; } public bool pictureRequested { get; set; } } public class ServoData { public int id { get; set; } public DateTime timeSent { get; set; } public byte[] bitmapData { get; set; } public string stringData { get; set; } } }
E, quindi, l’implementazione del servizio:
using System; using System.Collections.Generic; using System.ServiceModel; using System.ServiceModel.Activation; using System.ServiceModel.Web; using System.IO; using System.Drawing; using System.Web; namespace WcfRestServoCameraService { // Start the service and browse to http://:/Service1/help to view the service's // generated help page. NOTE: By default, a new instance of the service is created for each call; // change the InstanceContextMode to Single if you want a single instance of the service to process all calls. [ServiceContract] [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)] [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)] public class ServoCamera { // Implement the collection resource that will contain the SampleItem instances ServoDataControl control; List dataList; string appDataFolder = Path.Combine(HttpContext.Current.Request.PhysicalApplicationPath, "App_Data"); [WebGet(UriTemplate = "")] public Stream GetLastPhoto() { if (dataList == null) intializeServoDataControls(); try { if (dataList.Count != 0) { MemoryStream stream = new MemoryStream(dataList[dataList.Count - 1].bitmapData); control.pictureRequested = false; Image image = Image.FromStream(stream); MemoryStream jpgStream = new MemoryStream(); using (Image img = Image.FromStream(stream)) { stream.Position = 0; img.Save(jpgStream, System.Drawing.Imaging.ImageFormat.Jpeg); } control.pictureRequested = false; HttpContext.Current.Response.Cache.SetCacheability(HttpCacheability.NoCache); jpgStream.Position = 0; return jpgStream; } else { Image img = Image.FromFile(appDataFolder + "\\Gatto-di-Marco.bmp"); MemoryStream jpgStream = new MemoryStream(); img.Save(jpgStream, System.Drawing.Imaging.ImageFormat.Jpeg); control.pictureRequested = false; jpgStream.Position = 0; return jpgStream; } } catch (Exception ex) { control.pictureRequested = false; return null; } } [WebInvoke(UriTemplate = "{percent}", Method = "POST")] public ServoDataControl CreateControl(string percent) { if (control == null) { control = new ServoDataControl(); } control.timeSet = DateTime.Now; control.percent = uint.Parse(percent); control.pictureRequested = true; return control; } [WebGet(UriTemplate = "/state")] public ServoDataControl State() { if (control == null) { control = new ServoDataControl(); control.timeSet = DateTime.Now; control.percent = (uint)50; control.pictureRequested = false; } return control; } [WebInvoke(UriTemplate = "/data", Method = "POST")] public string CreateData(Stream dataStream) { if (dataList == null) { dataList = new List(); } byte[] buffer = new byte[32768]; MemoryStream ms = new MemoryStream(); int bytesRead = 0; int totalBytesRead = 0; do { bytesRead = dataStream.Read(buffer, 0, buffer.Length); totalBytesRead += bytesRead; ms.Write(buffer, 0, bytesRead); } while (bytesRead > 0); MemoryStream toImage = new MemoryStream(ms.ToArray()); ServoData item = new ServoData(); try { item.bitmapData = ms.ToArray(); item.id = dataList.Count; item.timeSent = DateTime.Now; dataList.Add(item); control.pictureRequested = false; // Set control to off. return "Image to bitmap data"; } catch (Exception ex) { return "error: " + ex.Message; } } [WebGet(UriTemplate = "{id}")] public Stream Get(string id) { try { if (dataList.Count > 0) { MemoryStream stream = new MemoryStream(dataList[int.Parse(id)].bitmapData); control.pictureRequested = false; return stream; } else { control.pictureRequested = false; return null; } } catch (Exception ex) { control.pictureRequested = false; return null; } } private bool removeItemById(int itemId) { return dataList.Remove(getItemById(itemId)); } private bool getAllDataItem(ServoData item) { if (item.id > 0) return true; else return false; } private ServoData getItemById(int itemId) { return dataList[itemId]; } private bool intializeServoDataControls() { if (control == null) { control = new ServoDataControl(); control.percent = 55; control.pictureRequested = false; control.timeSet = DateTime.Now; } if (dataList == null) { dataList = new List(); ServoData item = new ServoData(); item.id = dataList.Count; item.timeSent = DateTime.Now; Image img = Image.FromFile(appDataFolder + "\\Gatto-di-Marco.jpg"); MemoryStream jpgStream = new MemoryStream(); img.Save(jpgStream, System.Drawing.Imaging.ImageFormat.Jpeg); jpgStream.Position = 0; item.bitmapData = jpgStream.ToArray(); dataList.Add(item); } return true; } } }
Il WCF REST Web service template 4.0 funziona perfettamente quando si devono definire metodi GET e POST.
Infine, vediamo il file web.config per la configurazione del servizio.
<?xml version="1.0"?> <!-- I needed the multipleSiteBindingsEnable="true" to get this to work on hosting service.--> <!-- Configure the WCF REST service base address via the global.asax.cs file and the default endpoint via the attributes on the <standardEndpoint> element below. The empty name="" attribute makes the uri syntax simpler when using a remote server. Add the maxReceivedMessageSize and transferMode attributes for the .bmp data. -->
Per maggiori informazioni sul servizio WCF, potete consultare la relativa discussione sul forum di MSDN:
Il prossimo passo è lo sviluppo di un’applicazione client che utilizzi tale servizio per pilotare la camera. Un’applicazione client è descritto nel post: Controlling the Servo using a Windows Phone Application.