Introduction: USB Pendant for Universal Gcode Sender (UGS) for CNC Machining

I have a New Carve 1000 from CNC4Newbie and I use Universal Gcode Sender (UGS) to send my g-code to my controller. UGS has the ability to map keys to various actions, and this gives rise to the ability to have a wired or wireless "pendant" that I can use to move the machine around, run macros, etc. without having to use the main keyboard or mouse.

Originally, I found the Instructable by mr.incredible here: Wireless-UGS-Pendant. It was a great start, but I had a couple issues with the Jelly Comb numeric keypad that he used. Specifically, the program that maps keys from the keypad to the keystrokes to send to UGS had trouble reconnecting to the keypad after it went to sleep. And because it was wireless, I couldn't figure out how to keep it from going to sleep. So I switched to a wired USB keyboard that would always be on. Also, I use my machine a bit differently, so I updated the keys to follow my use case more closely. Regardless, it was a wonderful start.

Here, I'll detail how I made my pendant, including the hardware, software, keymapping for UGS, and artwork. It should be everything you need to get up and running with minimal effort.

Supplies

Step 1: Interpreting and Mapping Keys From the USB Keypad

You can do this with any keypad, but the images that I've created and the scripts are specific to the keypad in the Supplies section. Assuming you have this keypad, hook it up to your computer.

So theoretically you could just hook up the USB keypad and then map the keys in UGS to whatever functions you wanted. But the keys are common keys, like '5', that you may hit all the time, when you actually want to type a '5' and not move your y-axis, for example.

So what I did was use the program LuaMacros to intercept the keys from the USB keypad and map them to CTRL+ALT+{some letter}, which are likely not assigned to anything else and unlikely that you're going to accidentally hit them. With this program, you can capture keystrokes from a particular device like the USB keypad and ignore all keys from other devices, like your main keyboard. You could even hook up several keypads and have separate macros for each. The program will understand whether the numlock on the keypad is on, whether the fn key is being pressed, etc. It gives you a great deal of control.

Unfortunately, you need to write Lua script. What the...? Yeah, go figure. Lua script. Everybody knows Lua, right?!

Don't worry, I've done most of the heavy lifting already, and you can download my scripts in the sections below.

Step 2: Installation

To install LuaMacros just download the zip file from the Binary download section of the page and extract it. There is no install; it runs from wherever you put it.

When you run it, you'll see a window like shown above. The top section is where the Lua script goes, and the bottom section is the output.

Step 3: Finding Your USB Keypad ID

First, copy and paste the following code into the top window and press the play button (the blue triangle).

lmc_print_devices()

You'll see some output like shown in the image above.

In the output text, look for the code between the first and second ampersands. These are your keyboard identifiers.

In my case, my USB keypad is the top line:

<unassigned> : \\?\HID#VID_0603&PID_00F7&MI_00#7&D37C125&0&000#{884B96C3-56EF-11D1-BC8C-00A0C91405DD} [25494243]: keyboard

And my keyboard ID is PID_00F7.

Save these codes so you can try each of them until you get the right one. I'll show you how to do that, next.

Step 4: The Lua Script

Copy the code below into LuaMacros. (Or download the file from here.)

-- UGS Pendant
-- Todd Fjield, 09-27-2020
--lmc_print_devices()

local kbID='PID_00F7'

local key18 = 0 -- This is the Alt key
local key96 = 0
local key97 = 0
local key98 = 0
local key99 = 0
local key100 = 0
local key102 = 0
local key144set = 0


lmc_device_set_name('MACROS',kbID)

clear()

lmc.autoReload = true
lmc.minimizeToTray = true
--lmc_minimize()

-- Define callback for whole device
lmc_set_handler('MACROS',function(button,direction,ts,flags,makecode)
	if (button == 18) then key18 = direction end
	if (button == 96) then key96 = direction end
	if (button == 97) then key97 = direction end
	if (button == 98) then key98 = direction end
	if (button == 99) then key99 = direction end
	if (button == 100) then key100 = direction end
	if (button == 102) then key102 = direction end

	if (button == 144 and direction == 1) then
		if (key144set == 0) then 
			key144set = 1
		else 
			key144set = 0 
		end
	end

	if (direction == 1) then
		if     (button == 102 and key18 == 1 and key99 == 1) then withAlt(function() withCtrl(function() press('A') end) end) print('

Near the top, find this line and replace PID_00F7 with the ID of your USB keypad:

local kbID='PID_00F7'

Then press the save button (the little image of a floppy disk at the top left) and save the file as "UGS Pendant.lua". I save it in the same directory as LuaScript.exe.

) elseif (button == 96 and key18 == 1 and key100 == 1) then withAlt(function() withCtrl(function() press('B') end) end) print('(') elseif (button == 104 and key18 == 1 and key96 == 1 and key97 == 1 and key98 == 1) then withAlt(function() withCtrl(function() press('C') end) end) print('euro') elseif (button == 97 and key18 == 1 and key100 == 1) then withAlt(function() withCtrl(function() press('D') end) end) print(')') elseif (button == 101 and key18 == 1 and key96 == 1 and key97 == 1 and key102 == 1) then withAlt(function() withCtrl(function() press('E') end) end) print('yen') elseif (button == 46 and key144set == 0) then withAlt(function() withCtrl(function() press('F') end) end) print('delete') elseif (button == 27) then withAlt(function() withCtrl(function() press('G') end) end) print('esc') elseif (button == 111) then if(key144set == 1) then withAlt(function() withCtrl(function() press('H') end) end) print('/') else withAlt(function() withCtrl(function() press('I') end) end) print('numlock /') end elseif (button == 106) then if(key144set == 1) then withAlt(function() withCtrl(function() press('J') end) end) print('*') else withAlt(function() withCtrl(function() press('K') end) end) print('numlock *') end elseif (button == 109) then if(key144set == 1) then withAlt(function() withCtrl(function() press('L') end) end) print('-') else withAlt(function() withCtrl(function() press('M') end) end) print('numlock -') end elseif (button == 97 and key18 == 1 and key102 == 1) then withAlt(function() withCtrl(function() press('N') end) end) print('=') elseif (button == 36 and key144set == 1) then withAlt(function() withCtrl(function() press('O') end) end) print('home') elseif (button == 103) then withAlt(function() withCtrl(function() press('P') end) end) print('7') elseif (button == 38 and key144set == 1) then withAlt(function() withCtrl(function() press('Q') end) end) print('up') elseif (button == 104) then withAlt(function() withCtrl(function() press('R') end) end) print('8') elseif (button == 33 and key144set == 1) then withAlt(function() withCtrl(function() press('S') end) end) print('pgup') elseif (button == 105) then withAlt(function() withCtrl(function() press('T') end) end) print('9') elseif (button == 107) then -- Sending both keys so that we can perform two simultaneous functions. --if(key144set == 1) then withAlt(function() withCtrl(function() press('U') end) end) print('+') --else withAlt(function() withCtrl(function() press('V') end) end) print('numlock +') --end elseif (button == 9) then withAlt(function() withCtrl(function() press('W') end) end) print('tab') elseif (button == 37 and key144set == 1) then withAlt(function() withCtrl(function() press('X') end) end) print('left') elseif (button == 100 and key18 == 0) then withAlt(function() withCtrl(function() press('Y') end) end) print('4') elseif (button == 12 and key144set == 1) then withAlt(function() withCtrl(function() press('Z') end) end) print('no numlock 5') elseif (button == 101) then withAlt(function() withCtrl(function() withShift(function () press('A') end) end) end) print('5') elseif (button == 39 and key144set == 1) then withAlt(function() withCtrl(function() withShift(function () press('B') end) end) end) print('right') elseif (button == 102 and key18 == 0) then withAlt(function() withCtrl(function() withShift(function () press('C') end) end) end) print('6') elseif (button == 8) then withAlt(function() withCtrl(function() withShift(function () press('D') end) end) end) print('backspace') elseif (button == 35 and key144set == 1) then withAlt(function() withCtrl(function() withShift(function () press('E') end) end) end) print('end') elseif (button == 97 and key18 == 0) then withAlt(function() withCtrl(function() withShift(function () press('F') end) end) end) print('1') elseif (button == 40 and key144set == 1) then withAlt(function() withCtrl(function() withShift(function () press('G') end) end) end) print('down') elseif (button == 98 and key18 == 0) then withAlt(function() withCtrl(function() withShift(function () press('H') end) end) end) print('2') elseif (button == 34 and key144set == 1) then withAlt(function() withCtrl(function() withShift(function () press('I') end) end) end) print('pgdn') elseif (button == 99 and key18 == 0) then withAlt(function() withCtrl(function() withShift(function () press('J') end) end) end) print('3') elseif (button == 13) then -- Sending both keys so that we can perform two simultaneous functions. --if(key144set == 1) then withAlt(function() withCtrl(function() withShift(function () press('K') end) end) end) print('enter') --else withAlt(function() withCtrl(function() withShift(function () press('L') end) end) end) print('numlock enter') --end elseif (button == 45 and key144set == 1) then withAlt(function() withCtrl(function() withShift(function () press('M') end) end) end) print('insert') elseif (button == 96 and key18 == 0) then withAlt(function() withCtrl(function() withShift(function () press('N') end) end) end) print('0') elseif (button == 46 and key144set == 1) then withAlt(function() withCtrl(function() withShift(function () press('O') end) end) end) print('delete') elseif (button == 110) then withAlt(function() withCtrl(function() withShift(function () press('P') end) end) end) print('.') -- All keys above ALT+CTRL+SHIFT+P are available for sending multiple commands with a single keystroke.

end end

--print('CALLBACK :: Button ' .. button .. ', direction '..direction..', makecode: ' ..makecode.. ', flags '..flags)

if (direction == 0) then return end -- If any button is released, capture it. (Don't let ups pass through.) end)

-- Send any key (letter) as keystroke function press(key) lmc_send_input(string.byte(key), 0, 0) -- press lmc_send_input(string.byte(key), 0, 2) -- release end

-- Wraps function call with shift press function withShift(callback) lmc_send_input(16, 0, 0) -- press callback() lmc_send_input(16, 0, 2) -- release end

-- Wraps function call with alt press function withAlt(callback) lmc_send_input(18, 0, 0) -- press callback() lmc_send_input(18, 0, 2) -- release end

-- Wraps function call with alt press function withCtrl(callback) lmc_send_input(17, 0, 0) -- press callback() lmc_send_input(17, 0, 2) -- release end

Near the top, find this line and replace PID_00F7 with the ID of your USB keypad:

local kbID='PID_00F7'

Then press the save button (the little image of a floppy disk at the top left) and save the file as "UGS Pendant.lua". I save it in the same directory as LuaScript.exe.

Step 5: Testing the Script

Now, if you press the play button the script will run.

If you selected the right keyboard ID, you'll be able to press keys on the USB keypad and LuaMacros will output your keypress. (I did this for debugging purposes.) If when you hit keys on the USB keypad you don't see anything in the bottom window, replace the keyboard ID with the other keyboard and press play again to test it.

Once you're seeing a response in the bottom window, you've got LuaMacros configured properly!

Don't forget to save the file.

Step 6: Understanding the Key Mappings

The Qisan keypad is shown above, with the captured keystrokes overlayed on top of it. It's a bit small, but if you zoom in you should be able to see all the keystrokes. These keystrokes are all captured and interpreted in the Lua script to determine which keys are being pressed.

The three buttons on the top left have different functions if you hold down the fn key, and most of the other keys have different functions depending on whether or not the numlock key is pressed. Interestingly, when a single key is pressed there are often several keystrokes received. For example, when numlock is not on, pressing the 8 / up arrow key sends ascii key 144 down, 144 up, 38 down, 38 up, 144 down, 144 up. When numlock is on, the same key sends 104 down, 104 up.

I mapped each of these individual key outputs to CTRL+ALT+{some letter}, starting with a lowercase A in the top left corner (CTRL+ALT+A) all the way down to capital P on the lower right of the keypad (CTRL+ALT+SHIFT+P).

You don't actually need to know what keystroke each key is, because when you're setting up your key mappings in UGS, when you hit that key on the keypad UGS will tell you what key combination it receives. But if you want to modify the Lua script, the above mapping should help.

Step 7: Pendant Layout

I designed my pendant as follows, from top left to bottom right:

  • Calculator: Opens the calculator (this can't be overridden)
  • "G54" / $ = fn+Calculator: Calls the macro "G54" that puts the system into the G54 WCS
  • "rpm--" / '(': During sending, overrides the spindle rpm (reduces)
  • "G55" / € = fn+'(': Calls the macro "G55" that puts the system into the G55 WCS
  • "rpm++" / ')': During sending, overrides the spindle rpm (increases)
  • "G56" / ¥ = fn+')': Calls the macro "G56" that puts the system into the G56 WCS
  • fn: Function key, used to modify keys above
  • Recycle / delete: Soft reset
  • Numlock: Toggles the numlock. Various keys below have different functions if numlock is on.
  • "feed --" / esc: During sending, overrides the feed rate (reduces)
  • "feed ++" / '/' with or without numlock: During sending, overrides the feed rate (increases)
  • Spindle Toggle / '*' with or without numlock: During sending, when the send is paused, toggles the spindle on and off. Make sure to turn the spindle back on and give it time to spin back up before resuming the cut.
  • Home / numlock + '_': Homes the machine
  • Unlock / '_': Sends CTRL-X to GRBL
  • Tool Change / '=': Executes the macro "Tool Change" to move the machine to a convenient location
  • "X0" / '7': Zeroes the x-axis.
  • "G28" / home: Executes the macro "G28" that goes to the G28 position.
  • "Y0" / '8': Zeroes the y-axis.
  • "G28.1" / up arrow: Executes the macro "G28.1" to set the G28 position.
  • "Z0" / '9': Zeroes the z-axis.
  • "safe" / pgup: Executes the macro "Go To Safe" which moves the spindle back and to the left.
  • Outwards Arrows / '+': Multiplies the XY step size by 10, AND multiplies the Z step size by 10.
  • Probe / tab: Executes the macro "Full Probe Z" which executes a touch probe routine.
  • X right / '4' or left arrow: Moves the x-axis in the positive direction.
  • Y up / '5' with or without numlock: Moves the y-axis in the positive direction.
  • Z up / '6' or right arrow: Moves the z-axis in the positive direction.
  • "Stop" / back: Issues a stop command
  • X left / '1' or end: Moves the x-axis in the negative direction.
  • Y down / '2' or down arrow: Moves the y-axis in the negative direction.
  • Z down / '3' or pgdn: Moves the z-axis in the negative direction
  • Inwards Arrows / enter: Divides the XY step size by 10, AND divides the Z step size by 10.
  • Pause / '0' or insert: Pauses the send
  • Play / '.' or delete: Initiates send

There are a couple of things to consider:

  1. For some keys, like the X right (either '4' or left arrow) different codes are sent depending on whether numlock is on, but I wanted the same functionality, regardless. To get around this I mapped both keystroke (with numlock off and with numlock on) in UGS (as alternative shortcuts).
  2. Because I wanted a single keystroke to have the same effect as pressing the "Larger" or "Smaller" buttons on UGS, the '+' (Outwards Arrows) and the 'enter' (Inwards Arrows) send two keystrokes instead of just one (I send both the with and without numlock on keystrokes; you can see the comments in the Lua script) and I assign one of them to the XY and the other one to the Z for each. (There is no single keystroke in UGS to mimic hitting the "Larger" or "Smaller" buttons; it takes two to duplicate that functionality. I'm guessing that's an oversight.)

The way I use this is that I have numlock off most of the time. I only turn it on when I want to execute the rarer commands (like zeroing x, y, or z, or homing) that I don't want to hit accidentally when I'm jogging around, and then turn it off again right away. As an extra precaution, for example, I could remove the shortcut for play with numlock off, so that I would have to turn numlock on before the play button would do anything.

With the G54 / G55 / G56 commands, I tied these to the fn key so that I wouldn't accidentally change my WCS.

Step 8: Artwork

The artwork for the pendant can be downloaded below or here. The vinyl labels that I linked to above work very well and are printable with a standard inkjet printer. Then you can just cut them out with scissors or a paper shear and stick them on the keypad.

Step 9: UGS Macros

As mentioned in a previous section, several of the keystrokes are mapped to custom macros that I have written. These macros are as follows:

"G54"

G54;

"G55"

G55;

"G56"

G56;

"Change Tool"

G21;G90G53G0Z-20;G53G0X-743Y-805;G90;

"Go To G28"

G21;G91G28Z0;G28X0Y0;G90;

"Set G28"

G28.1;

"Go To Safe"

G21;G53G0Z-40;G91G0X-80Y260;G90;

"Full Probe Z"

G21;G90G53G0Z-40;G53G0X-743Y-805;G53G1Z-110F1000;G91G38.2Z-40F250;G0Z1;G38.2Z-2F50;G10L20P0Z9.06;G90G0Z20;G90;

Unfortunately, it doesn't seem like I can just export these macros for you to import, so you'll have to manually create them yourself if you want to use them.

Note that if you don't enter these macros before you import the key maps into UGS, some of the keys won't map and have any functionality. So you might consider at least creating blank macros before going any further, just to get the linkages, and then update them later.

Be careful with these macros! They are set up for specific locations on my machine that make sense for me, and they might not make sense for you.

Step 10: Mapping the Keys in UGS

Some of the keymaps for UGS are shown in the image above. You can see that some actions have multiple keystrokes assigned to them. This is so that the key works whether num lock is set or not.

You can individually map the keys yourself, or save yourself the trouble and download my keymaps here that you can directly import into UGS.

To use the file I provided, just save the zip file to disk, and then in the UGS settings click on the "Import..." button in the lower left of the settings dialog and point it to the file.

For the keymaps to set to the macros defined in the previous section, I've found that you need to define the macros and then restart UGS. Once you've restarted, you can import the Keymaps.zip file and it will map correctly to the macros.

Step 11: Test and Celebrate!

At this point, when UGS is running and LuaMacros is running the script, you should be able to press the keys on the USB keypad and UGS will execute the actions.

You'll notice in the script there is a line like this:

--lmc_minimize()

If you remove the "--" at the beginning of the line and save your script, then it will automatically minimize to the tray when it starts.

One final thing that you might consider is to create a shortcut to LuaMacros to automatically start your script. Just create a shortcut on your desktop and enter something like this for your target:

"C:\Install\LuaMacros\LuaMacros.exe" "C:\Install\LuaMacros\UGS Pendant.lua" -r

Update the paths as necessary. The "-r" flag automatically runs your script. You can put this shortcut in your startup folder and it will always be running silently in the background.

Hopefully the above has saved you some time and got you up and running with a USB pendant.

Good luck, and enjoy!