Walkthrough - Writing A Real-World Motor Plugin
This tutorial will walk you through creating a real-world motor device plugin. While the tutorial is presented in the context of the Phidget Low Voltage Motor Controller device, it also serves as a guide for other motor device types. The code is heavily commented to show you how you can use the code to help you write a plugin for your motor device. The actual functional code is about 90 lines.
Strategy
What we are doing: We are writing a MakeAffinity motor plugin for the Phidget Low Voltage Motor Controller (LV) device. Each LV has a number of motors for which we can set the acceleration and velocity. We can also read the acceleration and velocity of each motor, as well as two digital inputs per motor. The digital inputs are used for limit switches to stop the motors. We can also read the count of motors that the device has.
How the Phidgets API works: The Phidget's Manager provides a list of Phidgets devices that are present on your computer. There are many different types of Phidgets devices. And each device has a factory provided serial number that is unique across all Phidgets devices.
How our MakeAffinity plugin manager class will work: Our plugin's manager is responsible for creating instances of our plugin motor class. Therefore, our manager class will ask the Phidget's Manager how many LV devices are present, get their serial numbers, and for each device, instantiate a plugin motor class to control it.
How our MakeAffinity plugin motor class will work: Given a serial number for an LV device, our motor plugin class will open it, hold a reference to it, and work with it using the Phidgets API. Each motor class will then control its device appropriately when that class' methods are called. I.e. each class' methods will call its Phidgets device's methods to achieve what needs to be done. E.g. when our motor class' Initialise method is called, we will call the appropriate LV device's API methods to get it to a ready state.
You will need the following:
1. A working installation of MakeAffinity Client (setup here)
2. Visual C# Express (it's nice and free).
3. Your device plugged in.
The tutorial assumes that you are familiar with MakeAffinity and C#. The tutorial uses Visual C# Express 2005. Instructions and screen shots may be slightly different from other versions of Visual C# Express but are just as applicable. The tutorial also assumes that you are familiar with the device that you are writing the plugin for.
The tutorial should take you less than 50 minutes.
Visual C# Express will be referred to as VCE for the sake of brevity. MakeAffinity Client will be referred to as the client for the same reason.
The VCE project for this walkthrough can be downloaded from the Developer Center under Downloads.
The complete colour-coded source code for this walkthrough is shown here.
The main steps of writing a plugin are:
Step 1: Create a new class library project in VCE.
Step 2: Add a reference to the client's MAFFDevice.dll and your device's driver or library (Phidgets).
Step 3: Set the output path of your plugin project to your client's plugin folder.
Step 4: Add client Start & Stop build events to your plugin project.
Step 5: Create a MyManager class that inherits from MakeAffinity.Plugin.Manager.Manager.
Step 6: Create a MyMotor class that inherits from MakeAffinity.Plugin.Devices.Motor.
Step 7: Write code for the methods in MyMotor.
Step 8: Write code for the methods in MyManager.
Step 9: Compile and test your plugin.
Step 10: A debugging scenario.
Step 1: Create a new class library project in VCE
In VCE, go to File -> New Project -> choose Class Library -> use MyPlugin as name -> click OK.
When VCE completes creating your new project, it will display your source file.
Step 2: Add a reference to MakeAffinity Client's MAFFDevice.dll and your driver's library (Phidgets).
A) Go to Project -> Add Reference to bring up the Add Reference dialog box.
B) Click on the Browse tab, locate MakeAffinity Client's MAFFDevice.dll and click OK.
MAFFDevice.dll contains the device types you need to inherit from in your plugin.
The file should be located in C:\Program Files\MakeAffinity\makeaffinity\bin
Once added, MAFFDevice.dll will be listed under References in Solution Explorer.
C) Go to Project -> Add Reference to bring up the Add Reference dialog box again.
D) Click on the .NET tab, locate the Phidget21.NET for v2.x.x Runtime and click OK.
Note: Make sure you choose the one for v2.x.x of the .NET Runtime.
This walkthrough is presented in the context of a Phidgets device and it assumes that you have the Phidgets driver installed. If you are not writing the plugin for the Phidgets device then you will need to add references to the driver API of the device that your are writing the plugin for.
Step 3: Set the output path of your plugin project to the Client's plugin folder
A) Go to Project -> MyPlugin Properties to bring up the project properties dialog page.
B) Click on the Build tab.
C) Click on the Browse button under Output path to bring up the Select Output Path dialog box.
D) Locate and choose MakeAffinity Client's plugin folder.
This will make VCE copy the project's compiled output to the folder so the client will
always be able to use the latest compiled version.
The folder should be located at C:\Program Files\MakeAffinity\makeaffinity\DevicePlugins
Step 4: Add client Start & Stop build events to your plugin project
A) Go to Project -> MyPlugin Properties to bring up the project properties dialog page.
B) Click on the Build Events tab.
C) Add this to the Pre-build event command line (including the double-quotes):
"C:\Program Files\MakeAffinity\bin\ClientStop.exe"
D) Add this to the Post-build event command line (including the double-quotes):
"C:\Program Files\MakeAffinity\bin\ClientStart.exe"
The steps above are to stop the client each time VCE begins any compilation activities, and restart the client when compilation completes so you can see any new code changes in action.
Step 5: Create a MyManager class that inherits from MakeAffinity.Plugin.Manager.Manager
The original code in Class1.cs should look like this:
using System;
using System.Collections.Generic;
using System.Text;
namespace MyPlugin
{
public class Class1
{
}
}
|
A) Replace all of the code in the Class1.cs with the following:
|
using System;
using System.Collections.Generic;
using System.Text;
using MakeAffinity.Plugin;
namespace MyPlugin
{
public class MyManager: MakeAffinity.Plugin.Manager.Manager
{
}
}
|
The following is how the new code is different:
1) Added the directive: using MakeAffinity.Plugin;
2) Deleted Class1.
3) Added a MyManager class which inherits from MakeAffinity.Plugin.Manager.Manager.
B) Implement abstract class MakeAffinity.Plugin.Manager.Manager using IntelliSense.
In the code, click on MakeAffinity.Plugin.Manager.Manager. The first letter will then appear underlined.
Click the smart tag under MakeAffinity,
and click Implement abstract class 'MakeAffinity.Plugin.Manager.Manager'.
IntelliSense adds two override methods from the Manager class to the
MyManager class: Initialise and Shutdown.
Step 6: Create a MyMotor class that inherits from the MakeAffinity.Plugin.Devices.Motor type
A) Add a MyMotor class to the code so that the end result looks like the following.
|
using System;
using System.Collections.Generic;
using System.Text;
using MakeAffinity.Plugin;
namespace MyPlugin
{
public class MyManager: MakeAffinity.Plugin.Manager.Manager
{
public override void Shutdown()
{
throw new Exception("The method or operation is not implemented.");
}
public override void Initialise()
{
throw new Exception("The method or operation is not implemented.");
}
}
public class MyMotor : MakeAffinity.Plugin.Devices.Motor
{
}
}
|
B) Like before, implement abstract class MakeAffinity.Plugin.Devices.Motor using IntelliSense.
In the code, click on MakeAffinity.Plugin.Devices.Motor. The first letter will then appear underlined.
Click the smart tag under MakeAffinity,
and click Implement abstract class 'MakeAffinity.Plugin.Devices.Motor'.
IntelliSense adds 13 override methods from the Motor class to the MyMotor class:
BrandName, ModelName, SerialNumber, Initialise, Shutdown, MakeSafeAll, MotorCount, InputCount, SetVelocity, SetAcceleration, GetVelocity, GetAcceleration, GetInput
Step 7: Write code for the methods in MyMotor
IntelliSense has helped us to create the method stubs. We can now write the code for them. We will focus on MyMotor first. Implement the methods in MyMotor as shown below.
A) Create three class fields: One to remember the LV's serial number, one to hold the reference to the LV device, and one to remember when we have already hooked up the LV's error event to our error message handler.
|
/// <summary>
/// externalDeviceSerialNumber: Used to store the actual phidget
/// device's serial number.
/// The serial number is given to our motor class by our plugin's manager
/// </summary>
int externalDeviceSerialNumber;
/// <summary>
/// externalDevice: Used as a reference to the actual phidgets device
/// so the class can control it. A null value indicates that
/// we have not initialised it. (phidgets API field)
/// </summary>
Phidgets.MotorControl externalDevice = null;
/// <summary>
/// hasHookedUpEventHandler: Set to true when the class
/// has hooked up its error reporting method to the device's
/// error event. This will be used to prevent us from hooking
/// it up more than once.
/// </summary>
bool hasHookedUpEventHandler = false;
|
B) Implement the common device identification methods (BrandName, ModelName, SerialNumber) so the client will know how to identify our device. We'll use the following scheme for our brand name, model name, and serial number.
Phidget's device naming convention is: Name = "Phidget Low Voltage Motor Controller", SerialNumber = an integer. For our plugin, we'll return the following: BrandName: "Phidgets", ModelName: "Low Voltage Motor Controller", SerialNumber: Use the device's serial number.
|
/// <summary>
/// Phidget's device naming convention is:
/// Name = "Phidget Low Voltage Motor Controller"
/// SerialNumber = an integer;
///
/// For our plugin, we'll return the following for identification:
/// BrandName: "Phidgets"
/// ModelName: "Low Voltage Motor Controller"
/// SerialNumber: Use Phidget's own serial number
/// </summary>
public override string BrandName
{
get { return "Phidgets"; }
}
/// <summary>
/// Phidget's device naming convention is:
/// Name = "Phidget Low Voltage Motor Controller"
/// SerialNumber = an integer;
///
/// For our plugin, we'll return the following for identification:
/// BrandName: "Phidgets"
/// ModelName: "Low Voltage Motor Controller"
/// SerialNumber: Use Phidget's own serial number
/// </summary>
public override string ModelName
{
get
{
//(phidgets API calls)
string[] tokens = externalDevice.Name.Split(new char[] { ' ' });
string modelName = string.Empty;
for (int i = 1; i < tokens.Length; i++)
{
modelName += tokens[i] + " ";
}
modelName = modelName.Trim();
return modelName;
}
}
/// <summary>
/// Phidget's device naming convention is:
/// Name = "Phidget Low Voltage Motor Controller"
/// SerialNumber = an integer;
///
/// For our plugin, we'll return the following for identification:
/// BrandName: "Phidgets"
/// ModelName: "Low Voltage Motor Controller"
/// SerialNumber: Use Phidget's own serial number
/// </summary>
public override int SerialNumber
{
//(phidgets API call)
get { return externalDevice.SerialNumber; }
}
|
C) Implement the common device management methods (Initialise, Shutdown, MakeSafeAll) so the client can manage our device.
|
/// <summary>
/// Make the actual device ready and remember that we've done so to
/// prevent us from 'over initialising' the actual device.
/// </summary>
public override void Initialise()
{
// do not initialise again if already initialised.
// externalDevice == null means not yet initialised.
if (externalDevice != null) return;
// instantiate the actual device (phidgets API call)
externalDevice = new Phidgets.MotorControl();
//hookup to the external device's error reporting mechanism if
//we've not done so already
if (!hasHookedUpEventHandler)
{
//(phidgets API call)
externalDevice.Error += new Phidgets.Events.ErrorEventHandler (externalDevice_Error);
//(phidgets API call)
externalDevice.InputChange += new Phidgets.Events.InputChangeEventHandler (externalDevice_InputChange);
// set to true so we won't do it more than once by mistake.
hasHookedUpEventHandler = true;
}
// open the phidget device (phidgets API call)
externalDevice.open(externalDeviceSerialNumber);
// (phidgets API call)
externalDevice.waitForAttachment(1000);
// (phidgets API call)
if (!externalDevice.Attached)
{
// (phidgets API call)
externalDevice.close();
ReportErrorMessage(
this, 0, "Failed to initialise external device (PhidgetMotorControlLV)", null);
}
}
/// <summary>
/// Shutdown the actual device and set our internal variables
/// appropriately so if and when we need to initialise the
/// plugin we can do so correctly.
/// </summary>
public override void Shutdown()
{
// make safe/off the actual device
MakeSafeAll();
// close the external device: Phidgets use the close method for
// this. (phidgets API call)
externalDevice.close();
// set our reference to null so if our initialise method is
// called again we can proceed with initialisation.
// setting this to null also frees resources.
externalDevice = null;
}
/// <summary>
/// Put the actual device into a safe/off state
/// </summary>
public override void MakeSafeAll()
{
// stop all motors on this device
foreach (Phidgets.MotorControlMotor mot in externalDevice.motors)
{
mot.Velocity = 0;
mot.Acceleration = 50;
}
string tempStr = "MadeSafe: " +
BrandName + ", " + ModelName + ", " + SerialNumber;
ReportErrorMessage(this, tempStr);
}
|
D) Implement the methods that are specific to the motor plugin class (MotorCount, InputCount, SetVelocity, SetAcceleration, GetVelocity, GetAcceleration, GetInput).
|
/// <summary>
/// Read how many motors this device has
/// </summary>
public override int MotorCount
{
get { return externalDevice.motors.Count; }
}
/// <summary>
/// Set the velocity of a particular motor
/// </summary>
/// <param name="index"></param>
/// <param name="velocity"></param>
public override void SetVelocity(int index, double velocity)
{
externalDevice.motors[index].Velocity = velocity;
}
/// <summary>
/// Read the velocity of a particular motor
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
public override double GetVelocity(int index)
{
return externalDevice.motors[index].Velocity;
}
/// <summary>
/// Set the acceleration of a particular motor
/// </summary>
/// <param name="index"></param>
/// <param name="acceleration"></param>
public override void SetAcceleration(int index, double acceleration)
{
externalDevice.motors[index].Acceleration = acceleration;
}
/// <summary>
/// Read the acceleration of a particular motor
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
public override double GetAcceleration(int index)
{
return externalDevice.motors[index].Acceleration;
}
/// <summary>
/// Read how many digital inputs are there on this device
/// </summary>
public override int InputCount
{
get { return externalDevice.inputs.Count; }
}
/// <summary>
/// Read the state of a particular digital input
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
public override bool GetInput(int index)
{
return externalDevice.inputs[index];
}
|
E) Implement the constructor, error and input-change event handlers that we hooked up during Initialisation.
|
/// <summary>
/// The constructor takes the Phidget device's serial number
/// during instantiation. The number is given to us
/// by our plugin manager.
/// Our plugin manager gets it from the Phidget's manager.
/// </summary>
/// <param name="serialNumber"></param>
public MyMotor(int serialNumber)
{
// we'll use the serial number to find the device during
// initialisation.
this.externalDeviceSerialNumber = serialNumber;
}
/// <summary>
/// Our handler for the actual device's error event.
/// The method should convert the actual device's
/// error info into a format that the our
/// ReportErrorMessage method can use.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void externalDevice_Error(object sender, Phidgets.Events.ErrorEventArgs e)
{
// we translate and pass on the actual device's error info
ReportErrorMessage(this, e.Code, e.Description, null);
}
/// <summary>
/// Our handler for the motor device's input change event.
/// The device has two digital inputs for each motor output
/// and they are used typically for limit switches to stop
/// the motors. MakeAffinity processes the digital inputs
/// as limit switches.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void externalDevice_InputChange(object sender, Phidgets.Events.InputChangeEventArgs e)
{
ReportInputChange(this, new MakeAffinity.Plugin.Events.InputChangeEventArgs(e.Index, e.Value));
}
|
Step 8: Write code for the methods in MyManager
A) Create two class fields: one to hold the reference to the Phidgets Manager, and one to remember when we have already hooked up the Phidget Manager's error event to our manager's error message handler.
|
/// <summary>
/// hasHookedUpEventHandler: Set to true when the manager class
/// has hooked up its error reporting method to the Phidget
/// Manager's error event. This will be used to prevent
/// us from hooking it up more than once.
/// </summary>
bool hasHookedUpEventHandler = false;
/// <summary>
/// phidgetMgr: Our reference to the
/// Phidgets Manager (phidgets API field)
/// </summary>
Phidgets.Manager phidgetMgr = new Phidgets.Manager();
|
B) Implement the error event handler that we will hookup to Phidget Manager during our manager's Initialisation.
|
/// <summary>
/// Our handler for Phidget Manager's error event.
/// The method should convert the Phidget Manager's
/// error info into a format that our
/// ReportErrorMessage method can use.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void phidgetMgr_Error(object sender, Phidgets.Events.ErrorEventArgs e)
{
ReportErrorMessage(this, e.Code, e.Description, null);
}
|
C) Implement MyManager's Initialise method.
|
/// <summary>
/// Manager.Initialise will ask the Phidget's Manager
/// how many "Phidget Low Voltage Motor Controller"
/// devices are present,
/// get their serial numbers, and for each device,
/// instantiate a motor class to control it.
/// The motor objects are then stored in MAFFDevices
/// ready for the client to process
/// Manager.Initialise must put all devices in a
/// ready-to-use state.
/// </summary>
public override void Initialise()
{
//hookup to Phidget Manager error reporting mechanism if
//we've not done so already
if (!hasHookedUpEventHandler)
{
//(phidgets API call)
phidgetMgr.Error += new Phidgets.Events.ErrorEventHandler(phidgetMgr_Error);
// set to true so we won't do it more than once by mistake.
hasHookedUpEventHandler = true;
}
// open the phidget manager (phidgets API call)
phidgetMgr.open();
// Use the Phidget Manager to scan through the
// list of phidget devices present on the system.
// Instantiate a motor plugin class for each
// "Phidget Low Voltage Motor Controller" device found,
// using the device's serial number.
// Initialise each instantiated motor plugin to make it
// ready for use.
// Add instantiated motor plugins to MAFFDevices collection.
foreach (Phidgets.Phidget phidgetDev in phidgetMgr.Devices)
{
if (phidgetDev.Name.Contains("Phidget Low Voltage Motor Controller"))
{
MyMotor wrapper = new MyMotor(phidgetDev.SerialNumber);
wrapper.Initialise();
MAFFDevices.Add(wrapper);
}
}
}
|
D) Implement MyManager's Shutdown method
|
/// <summary>
/// Manager.Shutdown shuts down all devices and
/// clears the MAFFDevices collection.
/// IMPORTANT: Manager.Shutdown must put all devices
/// in a safe and off state when it completes
/// Further: Any allocated resources should also be made free.
/// </summary>
public override void Shutdown()
{
//shutdown all plugin objects in MAFFDevices
foreach (MakeAffinity.Plugin.Devices.Device dev in MAFFDevices)
{
dev.Shutdown();
}
//remove all plugin objects in MAFFDevices collection
MAFFDevices.Clear();
}
|
Step 9: Compile and test your plugin
The code is now complete and is here for your reference (Code Listing 4).
A) Compile your project: Go to Build -> Build Solution
VCE will launch the MakeAffinity Client when compilation completes without errors. If you encounter errors, compare your code to Code Listing 4.
B) At the client, click on Status button to see the status messages.
You should see messages indicating that your plugin is working.
C) See your plugin in action at the website:
Go to www.makeaffinity.com and log into your account.
Go to Make -> Constructs tab -> Select any construct.
Go to Actions tab -> Add Action -> Show My Devices.
Your plugin device should show up in the Device dropdownlist with the serial number, brand name, and model name that you have determined in your code.
You can now use your motor device to make a construct.
Change the Motor No., Direction, Velocity and Acceleration at the Make Editor and click Test and observe your device in physical action.
Step 10: A debugging scenario
Here, we will create errors in your plugin and show you how you can debug it.
A) Introduce an exception. Add the following code at the top of MyManager.Initialise:
throw new Exception("Exception 1");
B) Compile your project: Go to VCE -> Build -> Build Solution
VCE will launch the client when compilation completes.
C) Click on Client -> Status button to see status messages. You will see the following:
ERROR: Unable to load plugin in MyPlugin.dll, Message: Exception 1
D) More detailed information about the error is in the client's log file at C:\Program Files\MakeAffinity\makeaffinity\Logs\log.txt:
ERROR: Unable to load plugin in MyPlugin.dll, Message: Exception 1
Exception generated:
Message: Exception 1
Source: MyPlugin
Method: Void Initialise()
Stack Trace: at MyPlugin.MyManager.Initialise() in path\MyPlugin\MyPlugin\Class1.cs:line xx
at v.c() |
The detailed error message includes useful information such as the error message itself, the source assembly, namespace, class, method, and line number, as well as a stack trace.
The stack trace indicates that the fault is around line xx in the source code. This is where we deliberately introduced the exception for our debugging scenario.
Right now, the plugin device will not appear in your Make Editor because it has failed to load successfully. Removing the erroneous line and rebuilding the project will remedy this.
End of tutorial