Introduction
In this article you will see how to create a Microsoft Robotics (from now MR) service to use a game pad. This article is not a CCR or a DSS tutorial, and a good knowledge in these technologies will be very useful. In this article I assume you know what messages, queues and services are.
The base idea is quite simple: the service will poll the gamepad device every x
milliseconds. The gamepad device will be queried using managed direct input (.NET version) and will return values from -1
to 1
. So, for example the left upper corner will be (-1,1)
and the right lower corner will be (1,-1)
.
No timers or threads will be used to implement polling. I will use only messages and queues and the CCR Classes.
Background
Few words about hardware (the pad). I've used a Logitech Dual Action Gamepad (USB). This is a cheap and good device for gaming, precise enough to control an hobbyist robot. I never experienced Direct Input so I was forced, because of my poor knowledge, to "hard code" numeric values inside the code. I assume that these values are strictly related to my hardware, so, if you have a different gamepad change them accordingly.
The next version of this service will have configuration support using XML files.
In order to compile this solution, you must have the Microsoft.DirectX.DirectInput.dll file. You can find it in Directx SDK.
The project
To create an empty service, you have two ways: start from a blank solution or start using DssNewService.exe. I will show you the easy way: open MR command line and cd to samples directory, then launch DssNewService.exe /s:Pad This will create a new project which contains all the needed files in order to have a VS2005 solution with a DSS service. The project also has post build actions to generate Proxy classes.
The other files created are Manifest.xml and the PadTypes.cs files. PadTypes contains all the messages this service will use. There you define the "actions" your service will respond to.
Open the solution and add reference to Microsoft.DirectX.DirectInput.dll. Add:
using Microsoft.DirectX.DirectInput;
on Pad.cs. You are now able to use Managed Direct Input.
The service is split into three logical sections:
- Direct Input Init
- Direct Input Polling
- Message Notifications
Direct Input Init
private Device _gamepad = null;
...
DeviceList devlist = Manager.GetDevices(DeviceClass.GameControl,
EnumDevicesFlags.AttachedOnly);
foreach (DeviceInstance inst in devlist)
{
Guid g = inst.InstanceGuid;
_gamepad = new Device(g);
_gamepad.SetCooperativeLevel(_form.Handle,
CooperativeLevelFlags.Background |
CooperativeLevelFlags.NonExclusive);
}
if (_gamepad != null)
{
_gamepad.SetDataFormat(DeviceDataFormat.Joystick);
_gamepad.Acquire();
}
Now we can use _gamepad
to query the pad status to search for Sticks values or buttons status. The _form
is used only to get a "Handle" to init DirectInput
. This form could be use to provide graphical feedback of the pad.
ATTENTION! If I start debugging (F5) in VS2005, I get a "Loader Lock" warning. This is done by Managed debugging assistants (MDAs). So you can disable assistants as the article describes or you run and attach to process later for debugging (CTRL + ALT + P).
Direct Input Polling
Pad polling is split in two "blocks". The first one is based on a "timer": on every tick a gamepad read is performed and data is immediately sent to a queue. An Arbiter is activated on the queue: an anonymous delegate process messages and eventually notifies new "events" to the main port.
The timer is achieved using this snippet (freely taken from robotics examples).
private Port<DateTime> _timerPort = null;
...
_timerPort = new Port<DateTime>();
...
_timerPort.Post(DateTime.Now);
Activate(Arbiter.Receive(true, _timerPort,
delegate(DateTime sign)
{
Activate(
Arbiter.Receive(false, TimeoutPort(75),
delegate(DateTime time)
{
if (_gamepad != null)
{
JoystickState jstate = default(JoystickState);
_gamepad.Poll();
jstate = _gamepad.CurrentJoystickState;
_joyReads.Post(jstate);
}
_timerPort.Post(time);
}
)
);
}
));
Another arbiter is active on _joyReads
:
Activate(
Arbiter.Receive<JoystickState>(true, _joyReads,
delegate(JoystickState jsta)
{
})
);
That's it. Pad polling is almost complete. The commented code reads current sticks (L and R) values and normalize them in range (-1,1)
. Inside the omitted code, the interested "service" actions performed are similar to this snippet.
RightStickUpdate actR = new RightStickUpdate();
actR.Body.Stick.StickValueX = _state.RightStick.StickValueX;
actR.Body.Stick.StickValueY = _state.RightStick.StickValueY;
LogInfo("RightStickUpdate " + actR.Body.Stick.StickValueX,
actR.Body.Stick.StickValueY);
_mainPort.Post(actR);
RightStickUpdate
is a class defined in PadType.cs file.
public class RightStickUpdate :
Update<StickUpdateResponseRight, PortSet<DefaultUpdateResponseType, Fault>>
{
private StickStatus _stick;
[DataMember]
public StickStatus StickStatus
{
get { return _stick; }
set { _stick = value; }
}
public RightStickUpdate()
: base(new StickUpdateResponseRight())
{
_stick = base.Body.Stick;
}
}
The last step is inside the messages handler methods, for example:
[ServiceHandler(ServiceHandlerBehavior.Exclusive)]
public IEnumerator<ITask> RightStickUpdateHandler(RightStickUpdate rsUpd)
{
rsUpd.ResponsePort.Post(DefaultUpdateResponseType.Instance);
SendNotification(_submgrPort, rsUpd);
yield break;
}
Post on response port and send notification. The service flow is now complete. Inside the solution other messages are defined to fully handle sticks and buttons.
GUI
A GUI will be developed soon to give feedback on pad state. To show a form "inside" a service, use this snippet (actually commented in the source files). The PadInfoForm
is totally empty.
using Microsoft.Ccr.Adapters.WinForms;
...
PadInfoForm _form = new PadInfoForm();
WinFormsServicePort.Post(new RunForm(
delegate()
{
return _form;
}
));
The next improvement will be a GUI. If you have comments and suggestions, please post!
History
- September 2007: First release