Monday, 7 November 2011

ACS AET62 Fingerprint Reader and C#

I'm currently trying to interface to an ACS AET62 Fingerprint Reader from C#. The reader comes with an SDK, along with what appear to be extensive example applications. However, the applications contain bugs which the documentation does not help to resolve. I thought I would record the bugs I have found, and how I have resolved them.

ABSEnroll has a ref int parameter which is returned as a pointer to an ABS_BIR structure. This is declared in the provided BSTypes.cs file as containing a header followed by 2560 bytes of data. Unfortunately, this is not correct - it contains a header followed by up to 2560 bytes of data. The actual length of the whole structure (including the header) is contained in the first uint Length member of the structure.

This kind of variable length structure is quite common in C++, but there is no built-in way to marshal it correctly in C#.

The code provided in the SDK just calls Marshal.PtrToStructure on it - this will run off the end of the allocated memory, and assign garbage to the last (should be unused) bytes of the ABS_BIR structure. That's sort of OK in a 32-bit operating system (it is unlikely to try to access memory that doesn't exist), but in a 64-bit one with properly protected memory, it may well throw an exception.

The correct way to marshal the structure is in two parts - first marshal the header part with Marshal.PtrToStructure, then allocate the data byte array to the length therein (first subtracting the length of the header itself), and use Marshal.Copy to copy the remainder.

ppEnrolledTemplate = new BSTypes.ABS_BIR();
BSTypes.ABS_BIR_HEADER hdr = (BSTypes.ABS_BIR_HEADER) Marshal.PtrToStructure(ptr, typeof(BSTypes.ABS_BIR_HEADER));
ppEnrolledTemplate.Header = hdr;
int hdrlen = Marshal.SizeOf(hdr);
int bodylen = (int)(hdr.Length - hdrlen);
ppEnrolledTemplate.Data = new byte[bodylen];
Marshal.Copy(new IntPtr(tset + hdrlen), ppEnrolledTemplate.Data, 0, bodylen);

ABSVerify takes a reference to an IntPtr which is represented in C++ as **ABS_BIR - it is supposed to be an array of pointers to ABS_BIR. The example code provided just passed one ABS_BIR - it used Marshal to allocate a chunk of global memory and marshal the structure into it. The resulting IntPtr was passed as the ref parameter. This is all very fine if you only want to verify a single fingerprint, but if you want to determine which of a list of fingerprints is being scanned (as I did), it is no help at all.

My knowledge of C++ finally allowed me to figure out what was needed. - first, allocate an array of IntPtr (one for each ABS_BIR fingerprint scan you wish to pass). Then allocate memory for each structure into its corresponding IntPtr, and use Marshal to copy the structure into the allocated memory. Finally, pass a reference to the 0'th element of the array.

In actual fact, there is no need to Marshal the ABS_BIR structure during Enroll or Verify - it can be treated as an opaque array of bytes (with the length of the whole thing in the first 4 bytes) - other than the length, there is no other interesting data in the ABS_BIR structure.

Also, during Verify, you can make a single call to allocate enough memory for all the structures you want to pass, and calculate the values of the IntPtrs to pack the data into the single block of memory.

I hope this helps anyone else struggling with this SDK.

In response to anoop's comment, here is the code I use to call verify, to see if a scan corresponds to one of the scans in the database

BSTypes.ABS_OPERATION op = operation(1);
op.Timeout = 60000;
int res;
int matching_slot = 0;
res = BSApi.ABSVerify(_conn, ref op, (ushort)_templateIndex.Length, ref _templates[0], 
  ref matching_slot, 0); // BSTypes.ABS_FLAG_NOTIFICATION | BSTypes.ABS_FLAG_AUTOREPEAT);

To create the _templates array, you do this

int size = Marshal.SizeOf(typeof(BSTypes.ABS_BIR));
IntPtr[] templates = new IntPtr[number_of_elements];
templates[0] = Marshal.AllocHGlobal(size * number_of_elements);
byte[] zeroes = new byte[size];
int offset = 0;
int ind = 0;
foreach (byte[] b in list_of_scan_records) {
 if(ind > 0) templates[ind] = new IntPtr((int)templates[0] + offset);
 offset += size;
 Marshal.Copy(zeroes, 0, templates[ind], zeroes.Length);
 Marshal.Copy(b, 0, templates[ind], b.Length);

number_of_elements is the number of scans in the database, and list_of_scan_records is a list containing a byte array for each record


anoop singh said...

sir can you provide the code using absverify()
to verify fingerprint scan with all other scans in database

anoop singh said...

and sir how to make ABSEnroll() take only fingerswipe 1 time only and save it ??

MEL said...

Thanks sir; this blog has been very usefull for me. It save me a lot.

Can you provide me an other sample code for :

- ABSGrab
- ABSBinarizeSampleImage

MEL said...

Please also i have an ABS_IMAGE objet ; how to view it in a form or save it to the disk?

Nikki Locke said...

Sorry, anoop, I don't use those calls myself, and I haven't got spare time to do free consultancy right now.
I'm afraid you are on your own - but I hope my post will help you to see what to do.

MEL said...

Thanks for this reply.

Brightson JUpirik said...

hye sir, can you give some example code of yours how to interact with opaque data type?

Nikki Locke said...

Not really, Brightson, because I don't understand the question!

Brightson JUpirik said...
This comment has been removed by the author.
Brightson JUpirik said...

I'm very sorry sir for the bad English, it took me sometimes to understand what is the opaque data type. I understand about the two parts the header and the reminder. how do I apply it to write it into binary? this is the method content called out during enrolling.

FileStream objFileStream;
BinaryWriter objBinaryWriter;

int len = Marshal.SizeOf(ppEnrolledTemplate.Data);
byte[] data = new byte[len];
IntPtr ptr = Marshal.AllocHGlobal(len);

Marshal.StructureToPtr(ppEnrolledTemplate.Data, ptr, true);
Marshal.Copy(ptr, data, 0, len);

objFileStream = new FileStream(RecNo.ToString() + ".bin", FileMode.Create);
objFileStream.Write(data, 0, len);

I hope you can help me..

Brightson JUpirik said...

a sample code for the verify part?

Brightson JUpirik said...

That's Alright sir, I've already solved it!! weeks of headache was solved in one minor changed. i understand that based on what you said..

" That's sort of OK in a 32-bit operating system (it is unlikely to try to access memory that doesn't exist), but in a 64-bit one with properly protected memory, it may well throw an exception. "

I changed the solution platform to x86 or 32 bit, instead of any CPU or x64 or 64 bit. these pretty much solved my problem. thank you sir!