Troubleshooting javax.smartcardio Card Reads in Ignition

James Nishida11 min read
B&R AutomationOther TopicTroubleshooting
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

The failing case uses an Ignition Jython script with javax.smartcardio. It enumerates an SCM Microsystems Inc. SCR33x USB Smart Card Reader 0, detects the card, connects with terminal.connect("*"), sends 00 B0 00 00 00array('b', [0, -80, 0]), printed as the hex string 00-5000. The same card reads correctly from a CPython ctypes script that loads WinSCard.dll, then calls the SCM M-Card API in MCSCM.dll (MCardInitialize, MCardConnect, MCardReadMemory). That working script is the key clue. The card is read as a memory card through a vendor API, not with APDUs. Work through the checks below in order, and do not skip ahead until each reading is taken.

Check 1: Execution Scope and Terminal Selection

Before anything else, confirm that the script runs in the JVM on the machine where the reader is plugged in. javax.smartcardio talks to the local PC/SC service, so it only sees readers attached to the host running the code.

Where the script runs Which PC/SC service it sees
Designer Script Console The Designer workstation
Vision client event or component script The client workstation
Gateway event script, Perspective script The Gateway server

A reader at an operator station is invisible to Perspective and gateway-scoped scripts. Settle this architecture question first, because it decides where a helper process (Check 5) must be installed.

Next, stop selecting the reader by index. terminals.get(1) returns the second entry in the list, because Java lists are zero-based. The call returned the SCR33x without throwing, so at least two terminals are enumerated on that host. Their order is not guaranteed across reboots or re-plugs.

  1. Print every terminal with its index: loop for i in range(terms.size()) and print terms.get(i).getName().
  2. Select the reader by a name match, such as startswith("SCM"). The working ctypes script uses the same filter.
  3. Confirm that the printed name matches the physical reader holding the card before moving to Check 2.

Check 2: Raw Response, Status Word, and Byte Signedness

The output shows two separate problems, and both need fixing before any response can be trusted.

Signed bytes. Java byte is signed. The value 0xB0 arrives in Jython as -80, and '{:02X}'.format(-80) renders -50. That is why the hex string reads 00-5000. Mask every byte with before formatting.

Missing status word. ResponseAPDU.getData() strips the last two bytes, SW1 and SW2. Printing only getData() hides the one field that says whether the card executed the command. Always print getBytes() and getSW().

Build the APDU with the integer constructor, CommandAPDU(cla, ins, p1, p2, ne). This avoids the Jython array('B', ...) to Java byte[] conversion entirely. For an Le of 00, set Ne to 256.


Read the SW line and branch on it:

  • 9000 with data: the card is a processor card that accepts READ BINARY. Go to Check 3 only to confirm the file selection logic.

The failing case falls in the last branch. The data bytes 00 B0 00 are the command's own CLA/INS/P1 coming back.

Check 3: ATR and Card Class

A synchronous memory card has no command interpreter. It exposes raw memory zones that are clocked out through a simple wire protocol. To reach one through PC/SC, the reader driver must translate APDU-like requests into that protocol. If the driver does not do this for the card type in use, the request is never executed, and what comes back is noise or an echo.

The working script confirms this card class. It calls MCardConnect with byCardType = 8 and reads with MCardReadMemory using bMemZone = 0 and dwOffset = 0. That is memory-zone addressing, not file addressing.

Symptom Cause Next action
Hex string contains - characters Signed Java bytes formatted without masking Apply (Check 2)
Response bytes equal the command header Card or driver did not execute the APDU; memory card behind a PC/SC driver Check 4
IndexOutOfBoundsException on terminals.get() Index beyond the terminal list, or the reader is on another host Check 1
No terminals listed in a gateway or Perspective script Reader is attached to a client PC, not the Gateway Check 1; move the reader or the code
UnsatisfiedLinkError or a load failure on the DLL from Java DLL bitness differs from the JVM, or the dependency is missing Check 5
Helper returns non-zero codes from MCard* calls Wrong card type, reader name, or DLL build Section: Resolving Branch

Record the ATR printed in Check 2. Compare it against the card manufacturer's datasheet to identify the memory chip family. That identification decides whether any PC/SC path exists at all.

Check 4: A Native PC/SC Path to the Memory Card

Some PC/SC reader drivers expose synchronous memory cards through vendor-defined pseudo-APDUs, typically using class byte FF. Others expose them through escape commands sent with SCardControl. In javax.smartcardio, that second route maps to Card.transmitControlCommand(int controlCode, byte[] command). Connecting with protocol "DIRECT" instead of "*" opens the reader even when no usable protocol is negotiated with the card.

  1. Look up the SCR33x developer documentation from SCM for memory-card support under the standard PC/SC driver. Find the command set it documents for the chip identified by the ATR in Check 3.
  2. If the documentation lists pseudo-APDUs, send them through channel.transmit() using the diagnostic above. Do not move on until the status word reads success and the returned bytes differ from the command.
  3. If it lists escape IOCTL codes, send them with transmitControlCommand using the documented control code and payload. Never guess control codes; an undocumented escape can alter card memory.
  4. If the documentation offers neither for this card type, the supported access path is the M-Card API in MCSCM.dll. Go to Check 5.

In the working setup, the DLL only functioned when taken from the TEMPOMES driver installer. Treat that specific build as a dependency, and record its version on the target host.

Check 5: DLL Bitness Against the Ignition JVM

Jython cannot run the ctypes code, and javax.smartcardio does not reach the M-Card API. That leaves two ways to call MCSCM.dll: load it into the JVM through a native-access library such as JNA, or call it from a separate process. Bitness decides which one works.

The working script loads C:\Windows\SysWOW64\WinSCard.dll, and SysWOW64 holds 32-bit binaries. A 64-bit process cannot load a 32-bit DLL, so that script is running under 32-bit CPython. Inside a 32-bit process, Windows redirects its C:\Windows\System32\MCSCM.dll path to SysWOW64, so the M-Card DLL it actually uses is very likely 32-bit as well.

  1. Read the JVM data model on the host from Check 1. In the Script Console, run from java.lang import System; print(System.getProperty('sun.arch.data.model')). A result of 64 means a 64-bit JVM.
  2. Read the DLL machine type. From a Visual Studio developer prompt, run dumpbin /headers MCSCM.dll | findstr machine. 14C means x86 (32-bit); 8664 means x64.
  3. If both match, in-process native access is possible. It still needs a native-access library on the Ignition classpath, and every MCard* signature must be mapped by hand. A crash inside the DLL takes down the whole Designer, client, or Gateway JVM.
  4. If they differ, which is the expected case with a 64-bit Ignition runtime and a 32-bit M-Card DLL, in-process loading is impossible. Use the resolving branch below.

Even when the bitness matches, keep vendor DLL calls out of process on a Gateway. A native fault in a helper process costs one card read, not the Gateway.

Resolving Branch: Out-of-Process MCSCM Helper

The ctypes script already works. Wrap it so it prints one JSON object to stdout, run it under the same 32-bit CPython, and launch it from Ignition with java.lang.ProcessBuilder. Install both the helper and the driver on the host identified in Check 1.

  1. Install the 32-bit CPython that ran the original script, and the driver package that supplies MCSCM.dll, on the reader host. Before continuing, confirm that the original script still prints the card fields from a command prompt.
  2. Save the helper below. It keeps the evidence-proven call sequence and arguments, returns every call's result as unsigned hex, and releases the PC/SC context on exit. 0x0 is success for both the SCard* and MCard* calls. For the meaning of any other value, look it up in the WinSCard error list or in the M-Card API documentation.
  3. Run the helper by hand from a console. Do not move on until each step reports 0x0 and the hex field contains data with 1f separators.
  4. Add the Ignition launcher as a project library function, and call it from the event that should trigger a read.
# read_card.py -- run with the 32-bit CPython that ran the ctypes script
import ctypes, json

def code(r):
    return hex(ctypes.c_uint32(r).value)

out = {'steps': {}, 'cards': []}
ws = ctypes.WinDLL(r'C:\Windows\SysWOW64\WinSCard.dll')
ctx = ctypes.c_int(0)
out['steps']['SCardEstablishContext'] = code(
    ws.SCardEstablishContext(ctypes.c_int(0), None, None, ctypes.byref(ctx)))

buf = ctypes.create_string_buffer(800)
n = ctypes.c_uint32(800)
out['steps']['SCardListReadersA'] = code(
    ws.SCardListReadersA(ctx, None, buf, ctypes.byref(n)))
readers = [r for r in buf.raw.decode().split('\x00') if r.startswith('SCM')]

mc = ctypes.WinDLL(r'C:\Windows\System32\MCSCM.dll')
for name in readers:
    c = {'reader': name}
    hctx = ctypes.c_int(0); ver = ctypes.c_uint32(0)
    c['MCardInitialize'] = code(mc.MCardInitialize(
        ctx, ctypes.c_char_p(name.encode('utf-8')),
        ctypes.byref(hctx), ctypes.byref(ver)))
    hcard = ctypes.c_int(0)
    c['MCardConnect'] = code(mc.MCardConnect(
        hctx, ctypes.c_int(0), ctypes.c_int(8), ctypes.byref(hcard)))
    rbuf = (ctypes.c_ubyte * 200)()
    rlen = ctypes.c_int(100)
    c['MCardReadMemory'] = code(mc.MCardReadMemory(
        hcard, ctypes.c_int(0), ctypes.c_int(0),
        ctypes.byref(rbuf), ctypes.byref(rlen)))
    c['hex'] = bytes(rbuf[:rlen.value]).hex()
    c['MCardDisconnect'] = code(mc.MCardDisconnect(hcard, ctypes.c_int(0)))
    c['MCardShutdown'] = code(mc.MCardShutdown(hctx, ctypes.c_int(0)))
    out['cards'].append(c)

ws.SCardReleaseContext(ctx)
print(json.dumps(out))

The Ignition side waits for the process with a timeout, then reads stdout. The output is a few hundred bytes and fits in the pipe buffer, so waiting first does not deadlock. Replace the two placeholder paths; forward slashes avoid escaping issues.

from java.lang import ProcessBuilder
from java.io import BufferedReader, InputStreamReader
from java.util.concurrent import TimeUnit

PY32   = 'C:/path/to/python32/python.exe'   # 32-bit interpreter
HELPER = 'C:/path/to/read_card.py'

def runHelper(timeoutSec=10):
    pb = ProcessBuilder([PY32, HELPER])
    pb.redirectErrorStream(True)
    p = pb.start()
    if not p.waitFor(timeoutSec, TimeUnit.SECONDS):
        p.destroyForcibly()
        raise Exception('Card helper timed out')
    rd = BufferedReader(InputStreamReader(p.getInputStream()))
    lines = []
    line = rd.readLine()
    while line is not None:
        lines.append(line)
        line = rd.readLine()
    text = ''.join(lines)
    try:
        return system.util.jsonDecode(text)
    except:
        raise Exception('Helper output not JSON: ' + text)

The timeout is a design choice, not a vendor value. Set it above the longest read time you measure in step 3. Because stderr is merged into stdout, a Python traceback surfaces verbatim in the raised exception instead of vanishing.

Payload Parsing and Verification

The card stores its fields in memory zone 0, starting at offset 0. They are delimited by (ASCII unit separator). The first and last segments are boundary filler and get discarded. Seven fields follow in fixed order, and a single space marks an empty field. The working script also drops bytes, which is the erased state of unused memory. Keep the parse in Ignition so the helper stays a dumb byte pump.

FIELDS = ['First Name', 'Last Name', 'MI', 'User ID',
          'Alt User ID', 'Index', 'Employee ID']

def parseCard(hexstr):
    raw = ''.join([chr(int(hexstr[i:i+2], 16))
                   for i in range(0, len(hexstr), 2)
                   if hexstr[i:i+2].lower() != 'ff'])
    parts = raw.split('\x1f')[1:-1]
    info = {}
    for i, name in enumerate(FIELDS):
        v = parts[i] if i < len(parts) else ' '
        info[name] = None if v == ' ' else v
    return info

Commission the read path in this order:

  1. Call runHelper() from the Script Console on the reader host with a card inserted. Confirm that steps and every entry in cards report 0x0.
  2. Pass cards[0]['hex'] to parseCard(). Compare all seven fields against the values the original ctypes script prints for the same card.
  3. Remove the card and run again. Confirm that MCardConnect returns a non-zero code, and that your calling script treats it as "no card" rather than parsing stale data.
  4. Unplug the reader and run again. Confirm that the cards list comes back empty and the calling script reports a missing reader.
  5. Move the call into its production scope (Vision client script, or gateway event on the reader host). Repeat step 1 there, since the execution host and its permissions can differ from the Designer.
  6. Present three different badges in succession. Confirm that each returns its own Employee ID and that no read returns the previous card's data. This last check proves the connect, read, disconnect, and shutdown cycle completes on every pass.

FAQ

How do I fix negative hex values from javax.smartcardio in Jython?

Java bytes are signed, so 0xB0 arrives as -80 and formats as -50. Mask each byte before formatting: .

How do I tell if my smart card is a memory card instead of an APDU card?

Print card.getATR().getBytes() and match the ATR against the card datasheet. If a standard READ BINARY (00 B0 00 00 00) returns its own header bytes instead of a valid status word, the card or driver did not execute the APDU. That is typical of a synchronous memory card that needs a vendor API.

How do I call a 32-bit DLL like MCSCM.dll from Ignition?

A 64-bit JVM cannot load a 32-bit DLL. Wrap the calls in a script run by 32-bit CPython that prints JSON, and launch it with java.lang.ProcessBuilder from Ignition. Then decode the output with system.util.jsonDecode.

How do I read a USB smart card reader from a Perspective session?

Perspective scripts run on the Gateway, so they only see readers attached to the Gateway host. For a reader at an operator PC, run the read on that PC, for example in a Vision client or a local helper, and pass the result to the Gateway.

Why does terminals.get(1) pick the wrong smart card reader?

The terminal list is zero-based, and its order is not fixed, so index 1 is the second reader, whichever that happens to be. Iterate the list and select the reader whose getName() starts with the expected vendor string, such as SCM.

Back to blog