Wait for a card state change: SCardGetStatusChange()

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.