Using a Servo in a .NET Gadgeteer Camera Device

The Gadgeteer.Interfaces.PWMOutput interface can support the use of a servo to move mechanical parts of a device as needed by applications. This example uses the Gadgeteer.Interfaces.PWMOutput interface and a servomechanism to turn a camera through an arc of about 140 degrees.  The Pulse Width Modulation (PWM) input for this servo is obtained from a WCF REST Web service running on a remote IIS Server.  The user can direct the camera to take pictures over its range of motion from any device that can send a Web request to the service.  After the camera takes a picture, the device generates a POST request to upload the picture to the Web service so it is accessible remotely.  The post: Controlling the Servo using a Windows Phone Application describes a client application for the device and Web service.

The code that controls the servo is remarkably easy to write. You need an instance of Gadgeteer.Interfaces.PWMOutput for pulse width modulation control of the servo. You also need to know the PWM range of your servo, which is usually available online from the manufacturer. See the comments in the code for the range of my servo.  The PWM of my servo was given in milliseconds, but the .NET Gadgeteer PWMOutput interface uses nanoseconds. The numbers below are converted to nano seconds as required by the interface.


    GT.Interfaces.PWMOutput servo;
    static uint high = 2100000;
    static uint low = 900000;
    static uint delta = high - low;

Next, in the ProgramStarted method, initialize the objects needed to run the servo and camera. The Ethernet connection, of course, is needed to poll the Web service that sets the state of the servo. The application polls the Web service each tme a timer event fires to get the direction of the camera. After the servo turns the camera as specified by PWM from the service, the camera takes a picture.

        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");
        }

The complete code is shown in the following block, including the GetServoState() method and the SendPictureData(Gadgeteer.Picture picture) method. This post has been updated because the previous workaround is not needed for the Ethernet module, as in the post Interim Solution for Ethernet_J11D error.  This post has also been updated to use the new .NET Gadgeteer Networking API, including the HttpHelper.CreateHttpPostRequest and WebClient.GetFromWeb methods.  These methods are easier to use and do not block the dispatcher during GET and POST requests to the 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);
            }
        }

        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();
        }

    }
}

The core of this application is the code that polls the Web Service to get the PWM number that controls the servo. The number is a percentage of the delta between the top and bottom PWM settings, an unsigned integer that indicates a percentage of the range: 1%  – 100%.  The Web service returns the number to the application to be used in the SetPulse method of the Gadgeteer.Interfaces.PWMOutput interface.

Wiring the servo requires connecting the input wire to a pin on a P socket that supports pulse width modulation. The external power for the servo motor has to come from an external battery or transformer that supplies about 5 volts DC. The socket connections can be wired using the GHI Electronics Extender Module.

    uint pulse = low + (delta * percent / 100);
    servo.SetPulse(20000000, pulse);

Here is a schematic of the servo-control circuit from the GHI Electronics Extender module.  Following the schematic, a photo shows the wiring.  Only pin 9 and the ground on pin 10 are needed for the PWM circuit (click to enlarge).

Servo Module Schematic

Servo Module Schematic

.NET Gadgeteer Servo Wiring

.NET Gadgeteer Servo Wiring

REST Web Services to GET Servo PWM and POST Image Data
Implementing a REST Web service can be done with the WCF REST Web Service Template 4.0.  For more information on using the template project, see REST Web Service to Record Data from a .NET Gadgeteer Sensor Device and Remote Control of .NET Gadgeteer Device via REST Web Service.

Here are the WCF REST Web service methods used to set the servo state and retreive state information.

[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;
}

The Web service code that receives the picture from the camera module is shown in the following blocks.  Implementing the Web service to manage state information was easy, but getting the bitmap data into a format that works on this service took some discussion.  Marco Minerva is skilled in this area and provided some of this code in a discussion that you can follow on an MSDN WCF forum thread.

First the classes to store data and maintain state:

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; }
    }
}

Next the Web Service implementatation:

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://<machine_name>:<port>/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<ServoData> 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<ServoData>();
            }

            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 void deleteAllItems()
        {
            dataList.RemoveAll(getAllDataItem);
        }

        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>();
                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;
        }
    }
}

The WCF REST Web service template 4.0 works fine for the GET and for the POST methods.

The Web.config file for the Web service follows.

<?xml version="1.0"?>
<configuration>

  <system.web>
    <compilation debug="true" targetFramework="4.0" />
  </system.web>

  <system.webServer>
    <modules runAllManagedModulesForAllRequests="true">
      <add name="UrlRoutingModule" type="System.Web.Routing.UrlRoutingModule,
           System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
    </modules>
  </system.webServer>

  <system.serviceModel>
    <serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true"/>
    <standardEndpoints>
      <webHttpEndpoint>
        <!--
            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
        -->
        <standardEndpoint name="" helpEnabled="true"
                          automaticFormatSelectionEnabled="true" maxReceivedMessageSize="4194304"
                          transferMode="Buffered" />
      </webHttpEndpoint>
    </standardEndpoints>
  </system.serviceModel>

</configuration>

For more information about the WCF Web services, see the forum discussion:

It remains to show client applications.  See Controlling the Servo using a Windows Phone Application.

, , , , , ,

  1. #1 by oakninja@gmail.com on February 20, 2012 - 3:04 PM

    Exciting!

    Just bought a Spider Starter kit and find your blog very interesting! 🙂

    • #2 by Michael Dodaro on February 20, 2012 - 3:41 PM

      Good to hear! I’m having fun, and I think this platform is a game changer for .NET programmers who have been reluctant to tackle embedded development.

  2. #3 by Mike Frith on April 13, 2012 - 11:50 AM

    Mike,

    what servo are you using in this scenario?

    thanks
    Mike

  3. #5 by Mike Frith on April 13, 2012 - 1:39 PM

    Thanks Mike.

  1. Usare un servomeccanismo per orientare la videocamera con .NET Gadgeteer « Integral Design
  2. Controlling the Servo using a Windows Phone application « Integral Design
  3. me-bot: Gadgeteer Avatar Robot « Integral Design
  4. Using voice commands to control a servo « Integral Design

Leave a comment