Reading a 4-Byte RFID UID Through a Keyboard-Wedge/HID Reader in .NET
A .NET/WinForms design for reconstructing a 4-byte RFID identifier from eight keyboard-wedge hex characters, including Turkish Q/F mapping, state framing, WndProc, hooks, and Raw Input boundaries.
In 2014 I worked with an RFID reader whose application interface was neither a serial protocol nor a vendor SDK. After the device completed the RF-side card read, it emitted the resulting four-byte value as eight hexadecimal keystrokes. To the Windows application it behaved less like a specialized RFID peripheral and more like a very fast keyboard.
That distinction defines the problem solved by this code. It does not implement MIFARE authentication, ISO/IEC 14443, card-memory access, or raw USB HID report parsing. The RF transaction has already finished before this component sees anything; the job here is to turn a short 0-9/A-F keyboard sequence into one bounded application frame.
I model the boundary as:
RFID card
↓
reader
↓
keyboard-wedge / HID-like keyboard input
↓
Windows input path
↓
8 hexadecimal nibbles
↓
4-byte card value
↓
automation softwareThat distinction is the starting point for the entire design. The code discussed here is not a MIFARE authentication stack, an ISO/IEC 14443 implementation, a USB HID report parser, or a card-memory protocol. The RF transaction has already completed by the time this application layer sees the input.
I tested the same eight-nibble parser behind four Windows/WinForms input boundaries: ordinary keyboard events, IMessageFilter, a form WndProc, and WH_KEYBOARD_LL. The duplication was deliberate. A focused kiosk form and a multi-window automation application have different focus and message-pump behavior; the parser should not force both into one capture mechanism.
RFID and HID are different layers
RFID describes the radio-frequency identification technology between a card/tag and a reader. HID describes how an input device presents itself to the computer. An RFID reader can present its result through a USB keyboard-style interface; Windows then sees keyboard input rather than an RFID-specific application packet.
The practical attraction is deployment simplicity: if the reader can type its identifier into Notepad, a WinForms application can usually observe the same path without a proprietary client library.
The cost of that convenience is ambiguity. Human keyboard input and reader input can enter the same Windows keyboard stream. When the requirement changes from “recognize this short frame” to “prove which physical keyboard-class device produced it,” ordinary key messages are the wrong abstraction boundary; Raw Input is designed to retain device identity.
Why exactly eight hexadecimal characters?
The historical reader emitted a four-byte value in hexadecimal notation:
4 bytes × 2 hex digits per byte = 8 hex digitsWhen the eighth nibble arrives, the 32-bit value is complete.
This is a device/application contract, not a universal RFID rule. RFID UIDs and reader output formats can have other lengths. A reusable artifact should not generalize this implementation into the claim that RFID identifiers are always four bytes.
The Turkish Q/F keyboard-layout problem
A reader that outputs only 0123456789ABCDEF looks layout-independent at first. The digits mostly are. The letters are not necessarily so.
Windows has several distinct concepts in its keyboard pipeline: physical scan codes, virtual-key codes, translated characters, and the active keyboard layout. A keyboard-wedge reader can produce key events that result in the expected characters under one layout and different key identities under another.
The historical application therefore used separate 256-entry lookup tables for Turkish Q and Turkish F.
Under Turkish Q, the mapping is straightforward:
0..9 -> VK_0..VK_9
A..F -> VK_A..VK_FFor the Turkish F path observed in the original system, the hexadecimal letters arrived through:
A -> VK_U
B -> VK_OEM_2
C -> VK_V
D -> VK_E
E -> VK_OEM_1
F -> VK_AThe publication code keeps the constant-time lookup principle but expresses these mappings explicitly instead of hiding them inside long lookup strings.
The important engineering decision is to fix the problem at the same layer at which it is observed. At WM_KEYDOWN level, the input is a virtual-key value, not a final text character. A virtual-key mapping is therefore more deterministic than trying to repair a string after the fact.
Treat the receiver as a tiny finite-state machine
A quick implementation could put focus on a TextBox, wait for eight characters, then read Text. That couples the device protocol to the UI and introduces behaviors that are irrelevant to the actual problem.
The publication version keeps the parser as a small state machine rather than coupling it to a TextBox.
Each accepted hexadecimal virtual key becomes one four-bit nibble and is accumulated directly:
partial = (partial << 4) | nibbleAfter eight accepted nibbles, partial is already the final UInt32 card value.
No intermediate card string is required in the hot path. After the eighth accepted nibble the accumulated UInt32 is already the card value; the canonical eight-character X8 string is created only when a caller explicitly requests it through TryTakeHex.
For every accepted key, the steady-state work is approximately:
resolve layout
→ 256-entry lookup
→ shift
→ OR
→ increment countNo regex, LINQ, growing list, or per-key object is required.
Framing with time, not just character count
A keyboard-wedge reader typically sends its characters as a tight burst. A human can type the same characters, but with much less predictable timing.
Timing is not authentication, but it is a useful parser boundary. The historical code used a one-second threshold; the publication version retains that as the configurable default and measures it through TimeProvider, which also makes timeout behavior testable without depending on wall-clock sleeps.
A stale partial frame is cleared before the next accepted nibble.
This prevents a failed scan such as:
12ABfrom being silently combined with:
34CDseveral seconds later.
Common wedge suffix and modifier keys such as Enter, Tab, Shift, Ctrl, and Alt are ignored. An unexpected printable key resets the partial frame.
1. Ordinary KeyDown: the lowest-friction integration
The simplest WinForms path is a normal KeyDown handler.
With Form.KeyPreview = true, a form can observe keyboard input before child controls receive it in many common cases.
This approach has almost no platform interop and is suitable for a focused kiosk/card-entry screen.
Its weakness is exactly that: focus and control behavior matter. WinForms controls can preprocess input keys and dialog keys. Microsoft documents the KeyDown, KeyPress, KeyUp sequence and also notes that special keys can be processed by controls before ordinary keyboard events become useful to the application.
For a small focused form this is often sufficient. For a larger automation UI, the message pump can be a better place to observe the reader.
2. IMessageFilter: application-level observation before dispatch
IMessageFilter can inspect a Windows Forms message before the message is dispatched to a form or control. An implementation is installed with Application.AddMessageFilter.
That provides a useful application-wide input point without wiring every control.
The publication adapter watches WM_KEYDOWN and always returns false. It observes the message but does not consume it, so ordinary UI behavior continues.
The historical code also suppressed WM_ERASEBKGND in the same filter. That behavior was unrelated to RFID acquisition and could affect painting/flicker semantics, so it has been removed from the open-source component. A reader/parser should not change window background painting unless that is explicitly part of its responsibility.
3. WndProc: the explicit window-message boundary
A Windows Forms control's WndProc corresponds to the Windows WindowProc boundary. Overriding it makes the message-level dependency explicit:
protected override void WndProc(ref Message m)
{
reader.Observe(ref m);
base.WndProc(ref m);
}This is deterministic and local to the target window.
That locality can be an advantage in an application with a dedicated access-control form. It can also be a limitation if several windows must receive the reader or if capture is needed while the application is not foreground.
For messages the application does not own, the base implementation must remain in the chain. Microsoft's WinForms documentation explicitly recommends calling the base WndProc for messages that are not fully handled by the derived control.
4. WH_KEYBOARD_LL: low-level keyboard hook
A broader boundary is SetWindowsHookEx with WH_KEYBOARD_LL.
The system invokes a LowLevelKeyboardProc callback before a keyboard event is posted to a thread input queue. This is powerful, but it also carries more responsibility.
The publication hook is intentionally narrower than a general keyboard monitor.
First, it reads KBDLLHOOKSTRUCT.vkCode as the documented 32-bit field. The historical code read only the first byte from lParam; that happened to work for the normal virtual-key range but did not accurately express the native structure.
Second, the callback delegate is held by an instance field so the managed delegate remains rooted for the lifetime of the hook.
Third, every event continues through CallNextHookEx. The component is not an input blocker.
Fourth, it does not record arbitrary keyboard input. Only virtual keys relevant to the eight-nibble parser affect the state machine.
Finally, foreground-process capture is the default. System-wide capture is an explicit opt-in rather than the default behavior.
Microsoft also notes that a low-level keyboard hook callback must return promptly. Long-running work can cause serious input problems, and on supported Windows versions a hook that exceeds its timeout can be removed. This is another reason to do only constant-time frame assembly in the callback and leave database/UI work to the normal application flow.
Raw Input when physical device identity matters
The original code used classic keyboard messages and hooks. A modern design should also understand Raw Input.
An application registers a device class with RegisterRawInputDevices and receives WM_INPUT. The lParam value identifies a RAWINPUT record that can be read with GetRawInputData.
Unlike ordinary WM_KEYDOWN, Raw Input includes a device handle. GetRawInputDeviceInfo can therefore help distinguish the RFID reader from the human keyboard.
That is often the correct answer to the most important keyboard-wedge question:
Was this sequence generated by the reader or by the user's keyboard?
The library does not call RegisterRawInputDevices on behalf of its host. Within one process, Windows keeps a single registered target window for a given raw-input device class, so a reusable component can unintentionally replace the application's existing registration. Device registration therefore remains an explicit host-application decision.
WM_KEYDOWN, KeyPress, and Raw Input are not interchangeable
WM_KEYDOWN and KeyDown expose key identity near the virtual-key layer.
WM_CHAR and KeyPress represent character translation later in the keyboard pipeline.
Raw Input operates lower and can preserve device identity.
If the device already produces the correct characters under every supported layout, KeyPress can be extremely simple. If the Q/F issue must be corrected using virtual-key identity, WM_KEYDOWN is a better fit. If the physical reader must be distinguished from another keyboard, Raw Input is preferable.
There is no universally best capture method:
| Method | Scope | Focus sensitivity | Physical device identity | Complexity | |---|---|---:|---:|---:| | KeyDown/KeyPress | control/form | high | no | low | | IMessageFilter | application message pump | medium | no | low-medium | | WndProc | one window | medium | no | medium | | WH_KEYBOARD_LL | desktop/global hook | low | no | medium-high | | Raw Input | registered device class/window | configurable | yes | high |
A UID is an identifier, not proof of identity
Keyboard-wedge integration is an input mechanism, not a cryptographic trust boundary.
Software can synthesize keyboard events, and the feasibility of copying, emulating or replaying a card identifier depends on the RFID technology in use. The eight-character result is therefore best treated as:
identifier / lookup keynot automatically as:
authentication proofThe automation system should make authorization decisions through its own trusted database and policy layer.
Historical engineering context
My public project archive records card-access/RFID, serial communication, database, and GUI work at Afyon Kocatepe University IT Department in 2014, including a specific project for acquiring data from an RFID card reader.
That period also included RFID security software, UID conversion/encryption/generation utilities, database management for card-access systems, and serial-port tooling.
What survived from that 2014 code is not a particular WinForms event handler. It is the separation of concerns: identify what the device actually sends, resolve the Turkish Q/F mapping at the virtual-key layer, bound the frame in time, and expose one four-byte value without making the UI control part of the protocol.
That separation is still the part I would keep today. KeyDown, message filters, hooks and Raw Input are adapters around the application boundary. HexCardAssembler remains a small state machine with bounded state, a single pending card and synchronized updates; its responsibility ends when eight valid nibbles have become one UInt32.
References
- Microsoft,
IMessageFilter/PreFilterMessage: https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.imessagefilter - Microsoft,
Control.WndProc: https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.control.wndproc - Microsoft,
SetWindowsHookExandWH_KEYBOARD_LL: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowshookexw - Microsoft,
LowLevelKeyboardProc: https://learn.microsoft.com/en-us/windows/win32/winmsg/lowlevelkeyboardproc - Microsoft, Raw Input: https://learn.microsoft.com/en-us/windows/win32/inputdev/about-raw-input
- Microsoft,
WM_INPUT: https://learn.microsoft.com/en-us/windows/win32/inputdev/wm-input - Muhammet Ali Köker, Afyon Kocatepe University IT Department project archive: https://alikoker.com.tr/aku-bidb-staj
- Muhammet Ali Köker, project archive: https://alikoker.com.tr/projelerim
- Muhammet Ali Köker,
rfid-keyboard-wedge-csharp: https://github.com/alikoker/rfid-keyboard-wedge-csharp
Open Source Code
The publication implementation analyzed in this article is now available as open source on GitHub:
Source code: https://github.com/alikoker/rfid-keyboard-wedge-csharp
The repository contains HexCardAssembler, Turkish Q/F virtual-key mappings, KeyDown, IMessageFilter, WndProc, and WH_KEYBOARD_LL adapters, together with examples and tests. The article explains the hardware/software boundary and design decisions; the GitHub repository carries the executable implementation. The source archive does not embed duplicate article files, and repository metadata points to the canonical English article slug reading-rfid-keyboard-wedge-hid-dotnet.