Walkthrough - Writing A Real-World Relay Plugin
This tutorial will walk you through creating a real-world relay device plugin. While the tutorial is presented in the context of the Phidgets InterfaceKit 0/0/4 device, it also serves as a guide for other relay device types. The code is heavily commented to show you how you can use the code to help you write a plugin for your relay device. The actual functional code is about 75 lines.
Strategy
What we are doing: We are writing a MakeAffinity relay plugin for the Phidgets InterfaceKit 0/0/4 device. Each 0/0/4 has 4 switches for which we can set the state to true (on) or false (off). We can also read the state of each switch. We can also read the count of switches that the device has (4 in this case).
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 relay class. Therefore, our manager class will ask the Phidget's Manager how many InterfaceKit 0/0/4 devices are present, get their serial numbers, and for each device, instantiate a plugin relay class to control it.
How our MakeAffinity plugin relay class will work: Given a serial number for a 0/0/4 device, our relay plugin class will open it, hold a reference to it, and work with it using the Phidgets API. Each relay 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 relay class' Initialise method is called, we will call the appropriate 0/0/4 device's API methods to get the 0/0/4 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 MyRelay class that inherits from MakeAffinity.Plugin.Devices.Relay.
Step 7: Write code for the methods in MyRelay.
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 MyRelayPlugin 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 -> MyRelayPlugin 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 -> MyRelayPlugin 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 MyRelayPlugin
{
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 MyRelayPlugin
{
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 MyRelay class that inherits from MakeAffinity.Plugin.Devices.Relay
A) Add a MyRelay 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 MyRelayPlugin
{
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 MyRelay : MakeAffinity.Plugin.Devices.Relay
{
}
}
|
B) Like before, implement abstract class MakeAffinity.Plugin.Devices.Relay using IntelliSense.
In the code, click on MakeAffinity.Plugin.Devices.Relay. The first letter will then appear underlined.
Click the smart tag under MakeAffinity,
and click Implement abstract class 'MakeAffinity.Plugin.Devices.Relay'.
IntelliSense adds nine override methods from the Relay class to the MyRelay class:
BrandName, ModelName, SerialNumber, Initialise, Shutdown, MakeSafeAll, OutputCount, GetState, SetState
Step 7: Write code for the methods in MyRelay
IntelliSense has helped us to create the method stubs. We can now write the code for them. We will focus on MyRelay first. Implement the methods in MyRelay as shown below.
A) Create three class fields: One to remember the 0/0/4's serial number, one to hold the reference to the 0/0/4 device, and one to remember when we have already hooked up the 0/0/4'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 relay class by our plugin's manager
/// </summary>
int externalDeviceSerialNumber;
/// <summary>
/// externalDevice: Used as a reference to the actual phidgets device
/// so the relay class can control it. A null value indicates that
/// we have not initialised it. (phidgets API field)
/// </summary>
Phidgets.InterfaceKit externalDevice = null;
/// <summary>
/// hasHookedUpEventHandler: Set to true when the relay 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 InterfaceKit 0/0/4", SerialNumber = an integer. For our plugin, we'll return the following: BrandName: "Phidgets", ModelName: "InterfaceKit 0/0/4", SerialNumber: Use the 0/0/4's serial number.
|
/// <summary>
/// Phidget's device naming convention is:
/// Name = "Phidget InterfaceKit 0/0/4"
/// Type = "PhidgetInterfaceKit"
/// SerialNumber = an integer;
///
/// For our plugin, we'll return the following for identification:
/// BrandName: "Phidgets"
/// ModelName: "InterfaceKit 0/0/4"
/// SerialNumber: Use Phidget's own serial number
/// </summary>
public override string BrandName
{
get { return "Phidgets"; }
}
/// <summary>
/// Phidget's device naming convention is:
/// Name = "Phidget InterfaceKit 0/0/4"
/// Type = "PhidgetInterfaceKit"
/// SerialNumber = an integer;
///
/// For our plugin, we'll return the following for identification:
/// BrandName: "Phidgets"
/// ModelName: "InterfaceKit 0/0/4"
/// SerialNumber: Use Phidget's own serial number
/// </summary>
public override string ModelName
{
get
{
//(phidgets API call)
string[] tokens = externalDevice.Name.Split(new char[] { ' ' });
return tokens[1] + " " + tokens[2];
}
}
/// <summary>
/// Phidget's device naming convention is:
/// Name = "Phidget InterfaceKit 0/0/4"
/// Type = "PhidgetInterfaceKit"
/// SerialNumber = an integer;
///
/// For our plugin, we'll return the following for identification:
/// BrandName: "Phidgets"
/// ModelName: "InterfaceKit 0/0/4"
/// 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.InterfaceKit();
//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);
// 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 PhidgetInterfaceKit)", 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()
{
// set all of the actual device's relay's switches
// to off (phidgets API call)
for (int i = 0; i < externalDevice.outputs.Count; i++)
externalDevice.outputs[i] = false;
string tempStr = "MadeSafe: " +
BrandName + ", " + ModelName + ", " + SerialNumber;
ReportErrorMessage(this, tempStr);
}
|
D) Implement the methods that are specific to the relay plugin class (GetState, SetState, OutputCount).
|
/// <summary>
/// Set the state of the actual device's particular switch
/// to a particular state
/// </summary>
/// <param name="index"></param>
/// <param name="value"></param>
public override void SetState(int index, bool value)
{
// (phidgets API call)
externalDevice.outputs[index] = value;
}
/// <summary>
/// Read the current state of the actual device's
/// particular switch
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
public override bool GetState(int index)
{
// (phidgets API call)
return externalDevice.outputs[index];
}
/// <summary>
/// Read how many switches the actual device has
/// </summary>
public override int OutputCount
{
// (phidgets API call)
get
{
return externalDevice.outputs.Count;
}
}
|
E) Implement the constructor and error event handler that we will hookup 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 MyRelay(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);
}
|
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 InterfaceKit 0/0/4 devices are present,
/// get their serial numbers, and for each device,
/// instantiate a relay class to control it.
/// The relay 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 relay plugin class for each
// Phidget InterfaceKit 0/0/4 device found,
// using the device's serial number.
// Initialise each instantiated relay plugin to make it
// ready for use.
// Add instantiated relay plugins to MAFFDevices collection.
foreach (Phidgets.Phidget phidgetDev in phidgetMgr.Devices)
{
if (phidgetDev.Name == "Phidget InterfaceKit 0/0/4")
{
MyRelay wrapper = new MyRelay(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 2).
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 2.
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 relay device to make a construct.
Change the Switch No. and On/Off state 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 MyRelayPlugin.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 MyRelayPlugin.dll, Message: Exception 1
Exception generated:
Message: Exception 1
Source: MyRelayPlugin
Method: Void Initialise()
Stack Trace: at MyRelayPlugin.MyManager.Initialise() in path\MyRelayPlugin\MyRelayPlugin\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