Cheap Windows Jog/keyboard controller for CNCs

Cheap <$5 usb numpad to use for key macros and CNC job controllers. Primarily I'm using it to extend a remote for jogging around my G0704

Similar projects worth following
This is primarily a coding project. Basically buy a cheap HID USD Num pad or mini keyboard, or fill size keyboard or such from Amazon/Ebay/aliexpress, all that matters is that it works with windows and is HID.

The things are really cheap, there are spill proof versions for under 10 quid.

First i'll go over the code which is the primary part of this project it's mostly C, and i'm using Visual Studio 2013.

I'll go over the simple way of detecting a key press and intercepting it. Then i'll go over how to differentiate two or more keyboards/HID devices ,which is useful for a variety of projects.

How to on 3 different approaches to capturing key presses in windows from a HID/PS2 device and filtering it, in this case to drive a CNC jog controller.

Allows you to change, add or remove keypresses or even do something completely different

  • Going to the driver level

    charliex04/23/2015 at 18:05 0 comments


    So we looked at the basic ways with hooks and RAWINPUT.

    Windows has filter drivers, these basically sit on top of the existing drivers and can modify what the base driver does, simply a filter.

    MSDN Filter Drivers

    Software Needed

    We will be looking at upper device level filters. As I mentioned previously MSDN is a great resource, what we're looking for now is the DDK or as its known now the WDK, Windows Driver Kit, this needs to be installed alongside Visual Studio 2013, grab it here.

    WDK Downloads

    The Windows 8 WDK works for Windows 7, which is what i'm using.

    Debugger Notes

    Grab all that, set it up. Note the remote debugging client, these are very useful for drive development, since we're at level where you're expected to know whats going on and be able to debug system exceptions, so a second machine or a VM is very useful. VMWare for instance has a Visual Studio plugin to help remote debugging inside a guest OS, great for low level work.

    I also use a USB kernel debug cable for a second physical box, where i can't use or a VM isn't good enough.

    Setting up a Kernel Debugger

    You can use serial, firewire or USB for kernel debugging, but we likely won't need it for this.

    Back to MSDN

    Microsoft supplies code examples for almost every windows substem, and this no exception. We're looking at filter drivers, and there are ones for a PS/2 keyboard (kbdfilt) and a HID Mouse (firefly)

    kbdfilt This is an upper device filter driver sample for PS/2 keyboard. This driver layers in between the KbdClass driver and i8042prt driver and hooks the callback routine that moves keyboard inputs from the port driver to class driver. In its current state, it only hooks into the keyboard packet report chain, the keyboard initialization function, and the keyboard ISR, but does not do any processing of the data that it sees. (The hooking of the initialization function and ISR is only available in the i8042prt stack.) With additions to this current filter-only code base, the filter could conceivably add, remove, or modify input as needed.

    firefly Firefly is a KMDF-based filter driver for a HID device. Along with illustrating how to write a filter driver, this sample shows how to use remote I/O target interfaces to open a HID collection in kernel-mode and send IOCTL requests to set and get feature reports, as well as how an application can use WMI interfaces to send commands to a filter driver.

    KMDF is Kernel Mode Driver Framework , WMI is Windows Management Instrumentation.

    X64 Driver Signing and test sign mode

    The WDK samples are setup to self sign, but X64 windows won't like it unless you boot in TESTSIGNING mode, which means adding a flag to the loader. will test sign as well, and switch on testsign.

    The reason i posted this entry before it was finished, is because even though i have my own cert, i forgot to sign it before installing it on this PC , and promptly killed my keyboard off, since it required a reboot to install it. Should be using a VM. You can run the virtual keyboard, which lets you get keypresses again if you do dev on a single box.

    Setting up VMWare Player for driver debugging

    After installing WDK into VS2013, setup a VMware Player with a wIn 7 x64, set a username/password and then under the driver/test menu in VS configure a new machine. Tell it the name of the VM ( make sure sharing is on in windows, firewall on too ) copy over and install the remote test sign tools, run those then run the auto config in VS

    C:\Program Files (x86)\Windows Kits\8.1\Remote\ they're in this folder by default

    Windows 8.x is really needed to use the network debugging features in VS2013.

    VMWare by default doesn't pass in HID class devices, so to attach a keyboard or mouse directly to the VM , you'd need to add these to the VMX file

    usb.generic.allowHID = "TRUE"
    usb.generic.allowLastHID = "TRUE"

    Then reboot or have the VM powered off. Now you'll be able to attach...

    Read more »

  • Combing the keyboard hook with RAWINPUT

    charliex04/23/2015 at 17:03 0 comments

    Not Invented Here / Reinventing the Wheel

    Rather than do that, there is a project on codeproject talking about exactly this, and all the numerous pitfalls. It uses some of the API's we've already looked at.

    Combining Raw Input and keyboard Hook to selectively block input from multiple keyboards

    This more or less does what we want, but this method has a number of flaws. We can also look at using the non low level hooks, which requires a dll and hook injection (or access to the source code), look at WH_GETMESSAGE hooks for more information on that, since it combines both sets of information to make that determination.

    System Wide Hooking for the WM_CHAR Message

    So some solutions, not without their problems and one requiring hooking in into the apps, which may not work on all apps that use RAWINPUT, if we were just doing one app, and one machine this approach would likely work fine.

    Roundup so far

    So far we've looked at the global hook, which works really well but has the disadvantage of not being able to easily determine where the key came from and there are keys it can't catch, so it is basically a global search and replace. Then we looked at RAWINPUT which does tell us where it came from, but doesn't allow an easy substitution or removal of the messages. And now a project that married them together.

    As we can see there are ways of doing this, if you're just looking for a quick working (mostly) solution, there are a number of programs available that use these principles, here are a couple

    Keyboard Redirector

    hidmacros /LuaMacros

    AHK does it as well, if i recall, and uses the same methods, nearly all of these programs use a combination of the methods i've shown.

    We've got enough to do this now

    So if all you're looking for is a quick way to remap a second keyboard, stop here and grab those, enjoy your extra free time!

    So is there a better way ? Yes. I'll cover this next update.

  • Looking at RAWINPUT for more detail

    charliex04/23/2015 at 15:56 0 comments

    So in the previous example, it is a system wide low level keyboard hook that can change or remove key events from everything that generates output ( though some apps can bypass it) but what it doesn't tell us is where it came from. We need RAWINPUT for this. This will listen to nearly all the RAWINPUT's system wide.

    Make a normal WIN32 project , make these changes.

    i'll put these as complete examples on GitHub

    Add crtdbg.h to stdafx.h for the debug macros.

    Make InitInstance

    //   FUNCTION: InitInstance(HINSTANCE, int)
    //   PURPOSE: Saves instance handle and creates main window
    //   COMMENTS:
    //        In this function, we save the instance handle in a global variable and
    //        create and display the main program window.
    //        PostQuitMessage wont get the exit code out, since no pump, use debug RPT since no window yet.
    BOOL InitInstance ( HINSTANCE hInstance, int nCmdShow )
        HWND hWnd;
        hInst = hInstance; // Store instance handle in our global variable
        hWnd = CreateWindow ( szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
                              CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL );
        if ( !hWnd ) {
            return FALSE;
        // raw device count
        UINT nDevices = 0;
        // used for result code
        INT nResult;
        // get number of raw devices, including fake ones for RDP etc, this allows  us
        // to allocate the right amount of memory, since we're only asking for how many are attached.
        nResult = GetRawInputDeviceList ( NULL, &nDevices, sizeof ( RAWINPUTDEVICELIST ) );
        // this would be bad. possibly permissions.
        if ( nResult < 0 ) {
            _RPT1 ( _CRT_WARN, "failed to get raw input device list  %d\n", GetLastError() );
            PostQuitMessage ( 4 );
            return FALSE;
        _RPT1 ( _CRT_WARN,   "number of raw devices = %d\n", nDevices );
        // uhhh.
        if ( nDevices == 0 ) { PostQuitMessage ( 1 ); return FALSE; }
        // allocate enough memory for all the RAW devices reported
        pRawInputDeviceList = new RAWINPUTDEVICELIST[sizeof ( RAWINPUTDEVICELIST ) * nDevices];
        // failed ?
        if ( pRawInputDeviceList == NULL ) {
            _RPT1 ( _CRT_WARN, "failed to allocate memory for raw device list %d\n", GetLastError() );
            PostQuitMessage ( 2 );
            return FALSE;
        // zero it
        memset ( pRawInputDeviceList, 0, sizeof ( RAWINPUTDEVICELIST ) * nDevices );
        // now get the actual list of raw devices
        GetRawInputDeviceList ( pRawInputDeviceList, &nDevices, sizeof ( RAWINPUTDEVICELIST ) );
        // failed ?
        if ( nDevices < 0 ) {
            // Clean Up
            delete[] pRawInputDeviceList;
            _RPT1 ( _CRT_WARN, "failed to get list of raw devices %d\n", GetLastError() );
            PostQuitMessage ( 3 );
            return FALSE;
        for ( UINT i = 0; i < nDevices; i++ ) {
            // how much data do we need to allocate for the device name ?
            UINT nDeviceBufferLength = 0;
            nResult = GetRawInputDeviceInfo ( pRawInputDeviceList[i].hDevice,
                                              &nDeviceBufferLength );
            if ( nResult < 0 ) {
                _RPT1 ( _CRT_WARN, "failed to get raw input device name length %d\n", GetLastError() );
            // allocate memory for device name (wide char)
            WCHAR* wcDeviceName = new WCHAR[nDeviceBufferLength + 1];
            // failed?
            if ( wcDeviceName == NULL ) {
                delete[] pRawInputDeviceList;
                _RPT1 ( _CRT_WARN, "failed to allocate memory for raw device names %d\n", GetLastError() );
                PostQuitMessage ( 5 );
                return FALSE;
            // get the RAW device name
            nResult = GetRawInputDeviceInfo ( pRawInputDeviceList[i].hDevice,
                                              &nDeviceBufferLength );
            // failed?
            if ( nResult < 0 ) {
                _RPT1 ( _CRT_WARN, "failed to get actual raw device name %d\n", GetLastError() );
                // free the memory for the name
                delete[] wcDeviceName;
                // skip this one
            // setup buffer to receive device info
            RID_DEVICE_INFO ridDeviceInfo;
            ridDeviceInfo.cbSize = sizeof ( RID_DEVICE_INFO );
            // this tells the api how big the structure is
            nDeviceBufferLength = ridDeviceInfo.cbSize;
            // fetch the device info for this raw device
            nResult = GetRawInputDeviceInfo ( pRawInputDeviceList[i]...
    Read more »

  • Simple global keyboard hook

    charliex04/21/2015 at 22:59 0 comments

    Make a win32 console project in Visual Studio or so.

    add windows.h

    in main() add

    printf ( "setting global hook\n" );
    // Insert a low level global keyboard hook
    HHOOK hook_keyboard = SetWindowsHookEx ( WH_KEYBOARD_LL, ll_keyboardproc, 0, 0 );
    // now just the windows message pump
    MSG msg;
    while ( !GetMessage ( &msg, NULL, NULL, NULL ) ) {
     TranslateMessage ( &msg );
     DispatchMessage ( &msg );
    // unhook keyboard hook
    UnhookWindowsHookEx ( hook_keyboard );

    add this function, this gets called on every key press, up and down events (some keys need special handling)

    * ll_keyboardproc
    LRESULT CALLBACK ll_keyboardproc ( int nCode, WPARAM wParam, LPARAM lParam )
    // if keyup event , gets set to keyup flag
    int keyup = 0;
    // are we replacing/removing the key
    bool replace_key = false;
    // process it, if its an HC_ACTION otherwise pass on to OS
    if ( nCode == HC_ACTION ) {
    // key up/down etc
    switch ( wParam ) {
    case WM_KEYUP:
    case WM_SYSKEYUP:
    keyup = KEYEVENTF_KEYUP;
    // falls thru
    case WM_KEYDOWN:
    switch ( p->vkCode ) {
    //  process individual keys
    case 'Q':
    // send our replacement key (as an up or down message)
    keybd_event ( 'Z', 0, keyup, 0 );
    //remove the original key press
    replace_key = true;
    case 'Z':
    keybd_event ( 'Q', 0, keyup, 0 );
    replace_key = true;
    // pass on to OS
    if( replace_key == false ) {
    return CallNextHookEx ( NULL, nCode, wParam, lParam ) ;
    // remove the original key press.
    return 1;

    formatting gets removed in the code box.

    this will replace Q with Z and Z with Q, or q/z. You could also do something else, call a function, run an exe, output a digital line etc. Obviously this is also a simple key logger, you can also send multiple key presses, so now its a macro playback etc. there is some additional logic for sys keys etc, but keeping it simple.

    so pretty simple, install a global keyboard hook and process the messages. this would work if you had one keyboard and wanted to remap the keys, but it is less useful.

    next is using RAWINPUT to determine where the key came from

View all 4 project logs

Enjoy this project?



Similar Projects

Does this project spark your interest?

Become a member to follow this project and never miss any updates