I am "often" asked for a way to detect a card movement. There is a simple active loop solution but the correct answer is to use SCardGetStatusChange()
Simple but inefficient approach
It is possible to get a reader status using
SCardStatus(). It is then possible to imagine something like:
while(TRUE)
{
SCardStatus(hCard, NULL, 0, &dwState, NULL, NULL, NULL);
if (dwState & SCARD_PRESENT)
{
do stuff
}
sleep(1);
}
The problems with this code are:
- it consumes CPU cycles even when nothing happens
- when the code is executing the
sleep(1)
then no card movement is detected. So you can wait for up to 1 second before you detect a card event. You can shorted the delay to 0.1 second but then you will waste even more CPU cycles.
Of course this solution is not efficient and not recommanded.
Correct solution: SCardGetStatusChange()
The WinSCard API provides the function
SCardGetStatusChange() to wait for events.
You give a list of reader names and the function returns when the state of one (or more) reader changes, or a timeout occurs. I let you read the API documentation for the details.
You can interrupt the function using
SCardCancel() if needed. The function will then return before the expiration of the timeout.
Source code
#include <stdio.h>
#include <stdlib.h>
#ifdef __APPLE__
#include <PCSC/winscard.h>
#include <PCSC/wintypes.h>
#else
#include <winscard.h>
#endif
#define CHECK(f, rv) printf(f ":[0x%08lX] %s\n", rv, pcsc_stringify_error(rv))
int main(void)
{
LONG rv;
SCARDCONTEXT hContext;
LPTSTR mszReaders = NULL;
DWORD dwReaders;
rv = SCardEstablishContext(SCARD_SCOPE_USER, NULL, NULL, &hContext);
CHECK("SCardEstablishContext", rv);
#ifdef SCARD_AUTOALLOCATE
dwReaders = SCARD_AUTOALLOCATE;
rv = SCardListReaders(hContext, NULL, (LPTSTR)&mszReaders, &dwReaders);
CHECK("SCardListReaders", rv);
#else
rv = SCardListReaders(hContext, NULL, NULL, &dwReaders);
CHECK("SCardListReaders", rv);
mszReaders = calloc(dwReaders, sizeof(char));
rv = SCardListReaders(hContext, NULL, mszReaders, &dwReaders);
CHECK("SCardListReaders", rv);
#endif
if (dwReaders > 1)
{
printf("Using reader: %s\n", mszReaders);
SCARD_READERSTATE reader_states[] = {
{
.szReader = mszReaders,
.pvUserData = NULL,
.dwCurrentState = SCARD_STATE_UNAWARE,
.dwEventState = SCARD_STATE_UNAWARE,
},
};
/* first call to get the current state */
rv = SCardGetStatusChange(hContext, 0, reader_states, 1);
CHECK("SCardGetStatusChange", rv);
/* set the state we expect to change */
reader_states[0].dwCurrentState = reader_states[0].dwEventState;
/* wait for a state change, or a timeout after 3 seconds */
rv = SCardGetStatusChange(hContext, 3 * 1000, reader_states, 1);
CHECK("SCardGetStatusChange", rv);
printf(".dwEventState: 0x%08lX\n", reader_states[0].dwEventState);
}
#ifdef SCARD_AUTOALLOCATE
rv = SCardFreeMemory(hContext, mszReaders);
CHECK("SCardFreeMemory", rv);
#else
free(mszReaders);
#endif
rv = SCardReleaseContext(hContext);
CHECK("SCardReleaseContext", rv);
return 0;
}
Comments
The code uses the first reader returned by
SCardListReaders(). I wanted to keep the code very simple. Adding support of multi readers is left as an execise to the reader.
pcsc-lite on GNU/Linux supports the auto allocation. But the PC/SC API on macOS does not. That is why I use the
#ifdef SCARD_AUTOALLOCATE
.
Outputs
The outputs on GNU/Linux and macOS are slightly different. Can you find the difference (except the reader name)?
GNU/Linux
$ ./sample
SCardEstablishContext:[0x00000000] Command successful.
SCardListReaders:[0x00000000] Command successful.
Using reader: Gemalto PC Twin Reader 00 00
SCardGetStatusChange:[0x00000000] Command successful.
SCardGetStatusChange:[0x00000000] Command successful.
.dwEventState: 0x00050022
SCardFreeMemory:[0x00000000] Command successful.
SCardReleaseContext:[0x00000000] Command successful.
macOS
$ ./sample
SCardEstablishContext:[0x00000000] Command successful.
SCardListReaders:[0x00000000] Command successful.
SCardListReaders:[0x00000000] Command successful.
Using reader: Cherry KC 1000 SC Z
SCardGetStatusChange:[0x00000000] Command successful.
SCardGetStatusChange:[0x00000000] Command successful.
.dwEventState: 0x00000022
SCardReleaseContext:[0x00000000] Command successful.
.dwEventState
The difference is the
.dwEventState
value. On GNU/Linux the 16 upper bits are not 0.
From the
documentation:
dwEventState
also contains a number of events in the upper 16 bits (dwEventState
& 0xFFFF0000). This number of events is incremented for each card insertion or removal in the specified reader. This can be used to detect a card removal/insertion between two calls to SCardGetStatusChange()
In the GNU/Linux output we have .dwEventState: 0x00050022 so we got 5 card events for this reader. The events were card insertion, removal, insertion, removal and insertion.
The lower 16-bits value is 0x0022 in both cases and it indicates
SCARD_STATE_CHANGED and
SCARD_STATE_PRESENT.
Other language
SCardGetStatusChange() is for the C language API. Since it is possible to use the smart card API with
many languages (at least 23) each wrapper has (or should have) its own equivalent.
For example in Python you can use the low level API with the Python method
SCardGetStatusChange() or use the higher API with the
CardMonitor() class.
Conclusion
It is easy to use SCardGetStatusChange(). Please use it when needed.
It is also easy to misuse it. After writting this article I
(re)discovered that I already wrote about SCardGetStatusChange() in
"
How to use SCardGetStatusChange()?" two years ago. This
articles has different details and may also help you.