CEStuff.NET

It's not big, and it's not clever, like a dwarf with learning difficulties. But it's altruistic.

I had the misfortune to have to learn .NET, C#, eVC and build and program a CE handheld from scratch in a couple of months. I only had knowledge of C/C++ and Visual Studio. The information I have had to find out for myself and which sometimes took a heck of a time to search out is here.

I hope it is of use to someone. I wish it had been here for me.

You don't have to register to find the solution. I hate that.

You don't have to suffer people who know little, if anything, more than you moaning about top-posting and asking if you've turned the machine on. I hate that.

You don't have to send your email address. I hate that.

If you have anything to add, something which took you a while to work out and you want to share it, please feel free to email me at paulATbacktrackDOTorg. No spam, please. And if you have any questions which are proving a problem, let me have them too please, I'll post them below and we'll see if we can get the answers. That will make it all worthwhile.

Getting started:

First find your handheld device. Mine needed a barcode reader and WiFi, and needed to be ruggedised. The only reasonable priced one off-the-shelf was a PD264B/PD264XB from an Italian company, EIA, but it is flimsy and fails a lot. It had CE4. I won't list ALL the bad bits, if you want something really cheap and worth what you pay, get one. You will have to pay up front.

For the same money, we commissioned a company in the US to produce one for us, based on their existing ruggedised machine. The machine is superb and can be bought from envigour systems, based in Stirling, Scotland, UK. www.envigour.com. Go there first. It's CE5 too, which is nice.

From experience, it's more cost-effective to get nearly what you want and then alter it by adding components. You need to know a bit about electronics and/or interfacing and/or driver writing. But it's not hard.

The stuff you need:

You will need a decent setup to start programming your handheld. A PC with plenty of spare space for all the development software and service packs, look for around 20Gb of spare disk. And at least 1280 x 1024 resolution. And a good processor. And USB 2.

If you are going to be messing about at the hardware/driver level, you will need eVC4 (http://www.microsoft.com/downloads/details.aspx?familyid=1DACDB3D-50D1-41B2-A107-FA75AE960856&displaylang=en) and note the CD key at the bottom of that page, with SP4 (http://www.microsoft.com/downloads/details.aspx?FamilyID=4a4ed1f4-91d3-4dbe-986e-a812984318e5&displaylang=en). Load these two first, in that order.

If you load any other SDKs, for instance from a manufacturer, load them afterwards or they won't be available.

Obviously you can program your handheld now, in C/C++, the hard way. But if you want to use a higher-level language like VB or C#, you will need Visual Studio.NET. That costs a fortune, but that's the way it is.

You also need a version of toolhelp.dll for the handheld. It comes with most MS stuff, and is found by traipsing up the directories they install until you find it. Make sure it is for the right processor, we used a Samsung and an Intel ARMV4T-compatible which seems pretty much par for the course.

Connecting the Handheld

Usually you'll connect the Handheld via USB. That will need ActiveSync. SOME Handhelds, many in fact, won't install with ActiveSync higher than 3.5, so it might be worth loading that first, it ships with the Handheld. The latest at the time of writing is 3.7.1, again available from Microsoft on http://www.microsoft.com/windowsmobile/downloads/activesync37.mspx.

You also need the drivers for the Handheld, usually wceusbsh.inf and wceusbsh.sys (they come with the Handheld, nobody else's driver will work, usually), then having installed ActiveSync plug the USB connection to the Handheld and it will be installed for you. Point the New Hardware Wizard to the INF file when asked.

If you're using .NET, once you have done this, you will need to import keys to the registry because there is a bug (http://support.microsoft.com/default.aspx?scid=kb;en-us;813579) in .NET. The ProxyPorts.reg file is found at C:\Program Files\Microsoft Visual Studio .NET 2003\CompactFrameworkSDK\
WinCE Utilities\WinCE Proxy Ports Reg\ProxyPorts.reg. Just run it. And do it every time you have to reload ActiveSync.

If you're using eVC4 and you have any sort of Firewall like Norton or XP Pro's built in job, you need to let ActiveSync use some ports. Microsoft say they need 990, 999, 5678 and 5679 but enabling them didn't work for me. I had to disable the firewall. If anyone knows why I would love to hear about it.

Many Handhelds don't reconnect ActiveSync when you suspend/resume. Most will reconnect when you reset the Handheld. If this doesn't work, unplug the USB and plug it in again.

Programming with eVC4

No problems here, unless you're using eVC to write DLLs to do things that C# won't let you do. Passing parameters between eVC and C# (or VB) is normally very easy, you can use ref Int32, or return an Int32 value or whatever. But if you want anything of a decent size you will need some sort of referenced array. My favourite has been a TCHAR * so I can get any amount of stringy data back, for instance from a barcode scan, back to the C# app.

It's done easily, like this ...

eVC Code for the DLL :

BOOL WINAPI LibMain (HINSTANCE hinst, DWORD fdwReason, PVOID pvReserved) {
   return TRUE;

extern "C" __declspec (dllexport) int CALLBACK MyFunction(TCHAR * tc) {
   wcscpy(tc, somestring);
   return somevalue;
   }
}

C# code for the call :

[DllImport("NameOfTheDLLwhichIsInTheWindowsDirectoryOnTheHandheld")]
public static extern int MyFunction(StringBuilder sb); 
...
...
...
StringBuilder sb=new StringBuilder(HowBigTheTCHARarrayIs);
int sbReturn=MyFunction(sb);

which is nice, because you don't have to declare "unsafe" or use any strange marshalling.

Making a Noise

C# code for playing a sound on the Handheld

[DllImport("coredll")]
public static extern bool PlaySound( string szSound, IntPtr hMod, Int32 flags );
...
...
...

PlaySound("\\Windows\\critical.wav", IntPtr.Zero, 0x00020001);

The 0x00020001 is SND_FILENAME with SND_ASYNC, which will play the file asynchrously and your app will carry on. Those constants are in the header file MMSYSTEM.H.

The Registry

Accessing the Registry is easy too, using the Win CE functions. What is not so obvious is the declarations for them, which are here :

[StructLayout(LayoutKind.Sequential)]
public struct SECURITY_ATTRIBUTES { 
   public Int32 nLength; 
   public Int32 securityDescriptor; 
   bool bInheritHandle;
}

[DllImport("coredll")]
public static extern uint RegCreateKeyEx(
   uint hKey,
   string lpSubKey, 
   int Reserved, 
   string lpClass, 
   int dwOptions, 
   int samDesired, 
   ref SECURITY_ATTRIBUTES 
   lpSecurityAttributes, 
   ref uint phkResult, 
   ref int lpdwDisposition );

[DllImport("coredll")]
public static extern uint RegSetValueEx
   uint hKey, 
   string lpValueName, 
   int Reserved, 
   int dwType, 
   byte [] lpData, 
   int cbData );

Note that the other routines are similar, QueryValue and so on. And to call them ...

SECURITY_ATTRIBUTES sa=new SECURITY_ATTRIBUTES();
sa.nLength=Marshal.SizeOf(sa); // declared in System.Runtime.InteropServices
uint hKey=0;
int res=0;
uint HKEY_LOCAL_MACHINE = 0x80000002; // the others are in WINREG.H
uint i;
string valData="Drivers\\KBD"; // to access key "KBD" in "Drivers" in HKEY_LOCAL_MACHINE
char[] chData = valData.ToCharArray();
byte[] byValData = new byte[System.Text.UnicodeEncoding.Unicode.GetByteCount(chData) + 1];
byValData = System.Text.UnicodeEncoding.Unicode.GetBytes(chData);
string sKey=System.Text.UnicodeEncoding.Unicode.GetString(byValData, 0, byValData.Length);
i=RegCreateKeyEx(HKEY_LOCAL_MACHINE, sKey, 0, "", 0, 0, ref sa, ref hKey, ref res);

string valData1="BUTTON1"; // to set Value of "BUTTON1" in the above key
char[] chData1 = ((string)valData1).ToCharArray();
byte[] byValData1 = new byte[System.Text.UnicodeEncoding.Unicode.GetByteCount(chData1) + 1];
byValData1 = System.Text.UnicodeEncoding.Unicode.GetBytes(chData1);
string sKey1=System.Text.UnicodeEncoding.Unicode.GetString(byValData1, 0, byValData1.Length);
string valData2="";
byValData2 = new byte[System.Text.UnicodeEncoding.Unicode.GetByteCount(valData2) + 2];
byValData2 = System.Text.UnicodeEncoding.Unicode.GetBytes(valData2);
res=byValData2.GetLength(0)+2;
i=RegSetValueEx(hKey, sKey1, 0, 1, byValData2, res);

Talking to the Handheld from your app

Lots of functions are available via RAPI, the DLL which ActiveSync uses to communicate with the Handheld. And you can use them, most are very simple but there are some which are a bit elusive.

It's quite useful to be able to build all your app, including those bits which will get lost on a hard reset or when the Handheld battery goes completely flat. And it's quite simple to do. It involves writing a DLL to see if the remote end has finished what it's doing before you go on to the next task. It's a cow of a job, because although the RAPI function CeRapiInvoke is the way to get the information, fitting the parameters to it with C# is messy. I chose to do it using two DLLs, one to call CeRapiInvoke, and one to call that one with an easy C# linkage.

The first DLL which I call SetupHelperArmV4.DLL is largely plagiarised from Microsoft.

The second, which I imaginitively call MyDLL.DLL, is a very simple DLL which is given the Process Handle from CeCreateProcess and checks whether it has finished. It is inefficient, but works fine.

SetupHelper Code :

SetupHelper.def

LIBRARY SetupHelper

EXPORTS IsProcessComplete

SetupHelper.c

#include <WINDOWS.H>
#include <WINBASE.H>

STDAPI IsProcessComplete( DWORD cbInput, BYTE *pInput, DWORD *pcbOutput, BYTE **ppOutput, VOID *pIRAPIStream ) {
HRESULT hr = S_OK;
DWORD result = 0;
BYTE * pOut = NULL;
HANDLE hProc = *(HANDLE*)pInput;
DWORD wait = WaitForSingleObject(hProc, 0);
if (wait==WAIT_TIMEOUT) {
   result = FALSE;
   }
else {
   result = TRUE;
   }
pOut = LocalAlloc(LPTR, sizeof(DWORD));
if (pOut) {
    memcpy(pOut, &result, sizeof(result));
   *pcbOutput = sizeof(DWORD);
   *ppOutput = pOut;
   }
else {
   *pcbOutput = 0;
   hr = E_OUTOFMEMORY;
   }
return hr;
}

BOOL WINAPI DllMain( HANDLE hMod, DWORD dwReason, LPVOID lpvReserved ) {
return TRUE;
}

MyDLL Code :

MyDLL.cpp

extern "C" MYDLL_API int isProcessRunning(PROCESS_INFORMATION &pi);

MYDLL_API int isProcessRunning(PROCESS_INFORMATION &pi) {
typedef HRESULT (FAR PASCAL * pfnRapiInvoke)(
   LPCWSTR ,
   LPCWSTR ,
   DWORD ,
   BYTE *,
   DWORD *,
   BYTE **,
   void **,
   DWORD );

pfnRapiInvoke CeRapiInvoke;
HINSTANCE hInst=LoadLibrary("rapi.dll");
if (!hInst)
   return -2;
CeRapiInvoke =(pfnRapiInvoke)GetProcAddress(hInst, "CeRapiInvoke");
if (!CeRapiInvoke) {
   FreeLibrary(hInst);
   return -3;
}

BYTE *pData = NULL;
DWORD dwBytes = 4;
HRESULT hResult = CeRapiInvoke(
   L"SetupHelperARMV4.dll",
   L"IsProcessComplete",
   sizeof(pi.hProcess),
   (BYTE*)&pi.hProcess,
   &dwBytes,
   &pData,
   NULL,
   NULL);

if (hResult)
   return (UINT)hResult;

int result=0;
if (pData) {
   if (*(int*)pData) {
      result = (*(int *)pData);
      LocalFree(pData);
      }
   }
return result;
}

And calling this from C# is now simple ...

[StructLayout(LayoutKind.Sequential, Pack=4)] 
public struct ProcessInfo {
   public IntPtr hProcess;
   public IntPtr hThread;
   public int dwProcessId;
   public int dwThreadId;
}

[DllImport("rapi.dll", CharSet=CharSet.Unicode)]
public static extern Int32 CeCreateProcess(
   string lpApplicationName,
   string lpCmdLine,
   Int32 lpProcessAttribute,
   Int32 lpThreadAttribute,
   Int32 boolInheritsHandles,
   Int32 lwCreationFlags,
   Int32 lpEnvironment,
   Int32 lpCurrentDir,
   Int32 misc,
   ref ProcessInfo lpProcessInfo
);
...
...
...
ProcessInfo Information = new ProcessInfo();
CeCreateProcess("wceload.exe", "/noaskdest theFileYouWantToRun.cab", 0,0,0,0,0,0,0, ref Information);
uint retval=0;
while (retval==0) {
   Application.DoEvents(); 
   retval=isProcessRunning(ref Information);
   }
if (retval!=1) {
   MessageBox.Show("The Handheld has lost communication\n\nPlease select again once connected");
   return;
  
}