Walkthrough - Writing A TextBoxValues Plugin (Deprecated)
Note: The TextBoxValues Plugin is deprecated and is replaced by the TextBoxDropDownCheckBoxValues (TDC) Plugin.
This tutorial will walk you through creating a TextBoxValues plugin.
The TextBoxValues class is a simple yet highly versatile mechanism. It is used to present name-value input fields to the construct maker at the website. Each name-value pair will appear in the Make Editor as a label (the field name) and a textbox (where the user can input a value). The class simply passes the input values straight through to the plugin. What the plugin does with the values is entirely up to the plugin developer.
For this tutorial, we will write a plugin that lets the maker control Phidgets InterfaceKit 0/0/4 devices. The plugin will present the device to the user as 4 input fields - to set the states of the device's 4 relay switches.
While the tutorial is presented in the context of the Phidgets InterfaceKit 0/0/4 device, it also serves as a guide for other TextBoxValues plugins. The code is heavily commented to show you how you can use the code to help you write a TextBoxValues plugin for any device, system (e.g. a robot) or purpose. The actual functional code is about 80 lines.
Strategy
What we are doing: We are writing a MakeAffinity TextBoxValues 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).
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 TextBoxValues 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 TextBoxValues class to control it.
How our MakeAffinity plugin TextBoxValues class will work: Given a serial number for a 0/0/4 device, our TextBoxValues plugin class will open it, hold a reference to it, and work with it using the Phidgets API. Each TextBoxValues 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 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 MyTextBoxValues class that inherits from MakeAffinity.Plugin.Devices.TextBoxValues.
Step 7: Write code for the methods in MyTextBoxValues.
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 MyTextBoxValues class that inherits from MakeAffinity.Plugin.Devices.TextBoxValues
A) Add a MyTextBoxValues 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 MyTextBoxValues : MakeAffinity.Plugin.Devices.TextBoxValues
{
}
}
|
B) Like before, implement abstract class MakeAffinity.Plugin.Devices.TextBoxValues using IntelliSense.
In the code, click on MakeAffinity.Plugin.Devices.TextBoxValues. The first letter will then appear underlined.
Click the smart tag under MakeAffinity,
and click Implement abstract class 'MakeAffinity.Plugin.Devices.TextBoxValues'.
IntelliSense adds 8 override methods from the TextBoxValues class to the MyTextBoxValues class:
BrandName, ModelName, SerialNumber, Initialise, Shutdown, MakeSafeAll, GetFieldNames, SetFieldValues
Step 7: Write code for the methods in MyTextBoxValues
IntelliSense has helped us to create the method stubs. We can now write the code for them. We will focus on MyTextBoxValues first. Implement the methods in MyTextBoxValues 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 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.InterfaceKit 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 InterfaceKit 0/0/4", SerialNumber = an integer. For our plugin, we'll return the following: BrandName: "TextBoxValues (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: "TextBoxValues (Phidgets)"
/// ModelName: "InterfaceKit 0/0/4"
/// SerialNumber: Use Phidget's own serial number
/// </summary>
public override string BrandName
{
get { return "TextBoxValues (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: "TextBoxValues (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: "TextBoxValues (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 TextBoxValues plugin class (GetFieldNames, SetFieldValues).
|
/// <summary>
/// Return the collection of name-value field names and
/// their default values. The fields will be displayed
/// as textboxes at the Make Editor at the website.
///
/// This device has 4 switches, so we return 4 fields to
/// let the maker set the states of each switch.
/// </summary>
/// <returns></returns>
public override MakeAffinity.Plugin.Collections.FieldValues GetFieldNames()
{
// create a collection to store the fields that
// we'll be asking the maker to set.
MakeAffinity.Plugin.Collections.FieldValues fieldNamesAndDefaultValues =
new MakeAffinity.Plugin.Collections.FieldValues();
// add textbox fields for the maker to set the states of
// the device's switches, with default of off.
fieldNamesAndDefaultValues.Add("Switch No. 0 (true or false)", "false");
fieldNamesAndDefaultValues.Add("Switch No. 1 (true or false)", "false");
fieldNamesAndDefaultValues.Add("Switch No. 2 (true or false)", "false");
fieldNamesAndDefaultValues.Add("Switch No. 3 (true or false)", "false");
// return the set of fields to the client.
return fieldNamesAndDefaultValues;
}
/// <summary>
/// Receive the field and values from the maker at the website
/// and process them which ever way we like.
/// </summary>
/// <param name="fieldValues"></param>
public override void SetFieldValues(MakeAffinity.Plugin.Collections.FieldValues fieldValues)
{
//Get the inputs that the maker has specified
//and do work with them.
bool newState0 = Convert.ToBoolean( fieldValues["Switch No. 0 (true or false)"] );
bool newState1 = Convert.ToBoolean( fieldValues["Switch No. 1 (true or false)"] );
bool newState2 = Convert.ToBoolean( fieldValues["Switch No. 2 (true or false)"] );
bool newState3 = Convert.ToBoolean( fieldValues["Switch No. 3 (true or false)"] );
// use the received states to set the states of the device's switches
externalDevice.outputs[0] = newState0;
externalDevice.outputs[1] = newState1;
externalDevice.outputs[2] = newState2;
externalDevice.outputs[3] = newState3;
}
|
E) Implement the constructor and error event handler 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 MyTextBoxValues (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 MyTextBoxValues class to control it.
/// The MyTextBoxValues 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 MyTextBoxValues plugin class for each
// Phidget InterfaceKit 0/0/4 device found,
// using the device's serial number.
// Initialise each instantiated MyTextBoxValues plugin to make it
// ready for use.
// Add instantiated MyTextBoxValues plugins to MAFFDevices collection.
foreach (Phidgets.Phidget phidgetDev in phidgetMgr.Devices)
{
if (phidgetDev.Name == "Phidget InterfaceKit 0/0/4")
{
MyTextBoxValues wrapper = new MyTextBoxValues (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 5).
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 5.
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 TextBoxValues (Phidgets) InterfaceKit 0/0/4 device to make a construct.
Change the state of the switches 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