One of the most common requests from users is how to use external USB device for purpose of expanding TNG functionality. This could be either simple machine control using a gamepad or two way communication where device sends data, and TNG also responds back.
This project will describe how to use external USB device with PlanetCNC TNG software. We will use gamepad controller to jog CNC machine.
PlanetCNC software needs to “listen” to USB device anytime when it want to “say” something. Gamepad controller is a USB device that “screams” a lot, meaning, it sends constant stream of data. Data is arriving at the USB port non-stop.
Luckily, PlanetCNC TNG software offers set of expression functions, that help with handling and manipulation of USB data traffic.
Once we will successfully listen to our device and observe the data from gamepad controller, we will be able to program a expression script which will help us to jog CNC machine.
We recommend reading trough reference documentation of USB functions:
USB Functions
This document will cover following topics:
First, we need to identify USB device that we want to communicate with. Once our device is recognized from device list, we can obtain its USB device path. This path is our gateway to device data channel to which we will listen and read when anything arrives.
Under Help menu click: Show Log
Click HID devices button (red square) from the toolbar to display all HID devices connected to PC USB ports. Product description will help us identifying our gamepad controller(green square). After this we can easily see controllers USB path(yellow square).
Copy path of your device to clipboard and paste it to text editor for later use. Mark the path text, use right mouse button and click Copy.
Now that we have our USB device recognized, we need to see what kind of data device sends. Based on data structure and values, we will be able to manipulate suitable data packets.
Let find out, what is the size of data that this controller sends.
For this purpose we use usb_read() function.
Type into the MDI window:
NOTE: Use path of your device
path = "\\?\hid#vid_045e&pid_028e&ig_00#8&1fa5fd5b&0&0000#{4d1e55b2-f16f-11cf-88cb-001111000030}"; usb_open(path); usb_read(path, 0); usb_close(path);
If we run this, output window displays return value of 14. So this controller sends 14 bytes. This information is important once we use usb_addlistener() function.
usb_read(path, 0) = 14
Let also print the values using usb_readdata() function:
Type into the MDI window:
NOTE: Use path of your device
usb_readdata(path, 0);
If we run this, 14 byte values will be printed in the output window:
0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00
Lets print these values in loop so that any action on the controller will be immediately printed and we will be able observe and distinct which button/joystick affects certain byte(s) and in what way.
In your profile folder create file Expr_Gamepad_Controller.txt
Insert text below: NOTE: Use path of your device
#OnInit path = "\\?\hid#vid_045e&pid_028e&ig_00#8&1fa5fd5b&0&0000#{4d1e55b2-f16f-11cf-88cb-001111000030}"; usb_open(path); #Loop usb_readdata(path, 0); #OnShutdown usb_close(path);
Save file and restart PlanetCNC TNG sw. Output window will continuously print 14byte values. These values are printed in two rows, meaning it is difficult to track them with the naked eye.
0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x7F 0x00, 0x80, 0x00, 0x00, 0x00, 0x00 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x7F 0x00, 0x80, 0x00, 0x00, 0x00, 0x00 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x7F 0x00, 0x80, 0x00, 0x00, 0x00, 0x00 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x7F 0x00, 0x80, 0x00, 0x00, 0x00, 0x00 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x7F 0x00, 0x80, 0x00, 0x00, 0x00, 0x00
Lets try array_printdata() function, which will print data in one single line in the output window. Edit the original file with text below.
#OnInit path = "\\?\hid#vid_045e&pid_028e&ig_00#8&1fa5fd5b&0&0000#{4d1e55b2-f16f-11cf-88cb-001111000030}"; usb_open(path); hnd = array_new(); #Loop usb_readarray(path, hnd, 0); array_printdata(hnd, 14); #OnShutdown usb_close(path); array_delete(hnd);
Save file and restart PlanetCNC TNG sw. Output window will continuously print 14byte values. There, much better for the eye.
0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00
Byte numbering:
Now let's test controller buttons and observe which button affects which byte. Press buttons, move joystick and throttle to see which byte corresponds in the output window.
Image below describes the buttons of controller for better understanding:
After some testing, I was able to create a table which describes each buttons functionality and its corresponding byte with values.
NOT USED | byte 0 | ||
Left joystick | Horizontal | byte 1 | 0x00 - 0xFF |
Vertical | byte 3 | 0x00 - 0xFF | |
NOT USED | byte 4 | ||
Right joystick | Vertical | byte 5 | 0x00 - 0xFF |
Horizontal | byte 7 | 0x00 - 0xFF | |
NOT USED | byte 6 | ||
NOT USED | byte 8 | ||
Throttle | Left throttle | byte 9 | 0x80 - 0xFF |
Right throttle | 0x80 - 0x00 | ||
Buttons | A | byte 10 | bit: 1 |
B | bit: 2 | ||
X | bit: 3 | ||
Y | bit: 4 | ||
Left | bit: 5 | ||
Right | bit: 6 | ||
Select | bit: 7 | ||
Start | bit: 8 | ||
Direction pad | Up | byte 11 | 0x04 |
Up right | 0x08 | ||
Right | 0x0C | ||
Down right | 0x10 | ||
Down | 0x14 | ||
Down left | 0x18 | ||
Left | 0x1C | ||
Up left | 0x20 | ||
NOT USED | byte 12 | ||
NOT USED | byte 13 |
Examples below demonstrate jogging with gamepad buttons.
Implements jogging using direction pad buttons.
;Create new array for USB data storage. ;Enables USB communication ;Adds USB listener, which reads data from USB device and saves it in array. Each time device sends new data, array data is refreshed and callback function '#OnUsbGamepad' is executed. #OnInit hnd = array_new(); path = "\\?\hid#vid_045e&pid_028e&ig_00#9&ad497a1&0&0000#{4d1e55b2-f16f-11cf-88cb-001111000030}"; usb_open(path); usb_addlistener(path, "#OnUsbGamepad", hnd, 14); #OnShutdown usb_close(path); usb_remlistener(path); array_delete(hnd); #OnUsbGamepad ; Parse USB data. Received data is stored in array. .arg2 argument holds array handle. ; According to the 'table', byte 11 represents 'Direction pad' values. We store it in private variable .dir. .dir = array_getdata(.arg2, 11); ; From .dir variable we calculate direction represented as angle and store its value to private variable .angle. .angle = -1; if(.dir == 0x04, .angle = 0); if(.dir == 0x08, .angle = 45); if(.dir == 0x0C, .angle = 90); if(.dir == 0x10, .angle = 135); if(.dir == 0x14, .angle = 180); if(.dir == 0x18, .angle = 225); if(.dir == 0x1C, .angle = 270); if(.dir == 0x20, .angle = 315); ; Now that we have angle values we can calculate x and y axis jogging parameters. These are stored in private variables .ax and .ay. .ax = 0; .ay = 0; if(.angle == 0, .ay = +1); if(.angle == 180, .ay = -1); if(.angle == 90, .ax = +1); if(.angle == 270, .ax = -1); ; If we jog in diagonal directions, factor 0.707 is used to compensate the longer distance. ;0.707 = 1/sqrt(2) if(.angle == 45, exec(.ax = +0.707, .ay = +0.707)); if(.angle == 135, exec(.ax = +0.707, .ay = -0.707)); if(.angle == 225, exec(.ax = -0.707, .ay = -0.707)); if(.angle == 315, exec(.ax = -0.707, .ay = +0.707)); ; Finally axis jogging parameters are sent to 'jog' function to perform jogging. jog(0, .ax, .ay, 0);
Implements jogging using left or right joystick.
#OnInit hnd = array_new(); path = "\\?\hid#vid_045e&pid_028e&ig_00#9&ad497a1&0&0000#{4d1e55b2-f16f-11cf-88cb-001111000030}"; usb_open(path); usb_addlistener(path, "#OnUsbGamepad", hnd, 14); #OnShutdown usb_close(path); usb_remlistener(path); array_delete(hnd); #OnUsbGamepad ; Parse USB data. Received data is stored in array. .arg2 argument holds array handle. ; According to the 'table', bytes 1,3,5,7 represent left and right joystick values. We store them in private variables .lx, .ly and .rx, .ry. ; Depending on joystick position, jog parameters are calculated in range [-1...1]. .lx = (array_getdata(.arg2, 1) - 0x80) / 0x80; .ly = (array_getdata(.arg2, 3) - 0x7F) / 0x80; .rx = (array_getdata(.arg2, 5) - 0x80) / 0x80; .ry = (array_getdata(.arg2, 7) - 0x7F) / 0x80; jog(0, .lx, -.ly, 0); jog(0, .rx, -.ry, 0);
Implements jogging with ABXY buttons
#OnInit hnd = array_new(); path = "\\?\hid#vid_045e&pid_028e&ig_00#9&ad497a1&0&0000#{4d1e55b2-f16f-11cf-88cb-001111000030}"; usb_open(path); usb_addlistener(path, "#OnUsbGamepad", hnd, 14); #OnShutdown usb_close(path); usb_remlistener(path); array_delete(hnd); #OnUsbGamepad ; Parse USB data. Received data is stored in array. .arg2 argument holds array handle. ; According to the 'table', byte 10 represents ABXY button values. We store it in private variable .btn. .btn = array_getdata(.arg2, 10); ; To obtain particular button state, we use bitwise AND operation. ; Bitwise AND operation between byte 10 (stored in variable .btn) and button bit will return 1 if bit is set (button is pressed) and 0 otherwise. ; Button states are stored in private variables: .btnA, .btnB, .btnX, .btnY. .btnA = .btn & 1; .btnB = .btn & 2; .btnX = .btn & 4; .btnY = .btn & 8; ; .ax and .ay values are initialized to zero. They are used to set jogging direction. ; When button state is 1, .ax and .ay are set to +1 or -1, depending on desired jogging direction. .ax = 0; .ay = 0; if(.btnX, .ax = -1); if(.btnB, .ax = +1); if(.btnA, .ay = -1); if(.btnY, .ay = +1); jog(0, .ax, .ay, 0);
Implements jogging with ABXY buttons and left throttle.
#OnInit hnd = array_new(); path = "\\?\hid#vid_045e&pid_028e&ig_00#9&ad497a1&0&0000#{4d1e55b2-f16f-11cf-88cb-001111000030}"; usb_open(path); usb_addlistener(path, "#OnUsbGamepad", hnd, 14); #OnShutdown usb_close(path); usb_remlistener(path); array_delete(hnd); #OnUsbGamepad ; Parse USB data. Received data is stored in array. .arg2 argument holds array handle. ; According to the 'table', byte 10 represents ABXY button values. We store it in private variable .btn. .btn = array_getdata(.arg2, 10); ; To obtain particular button state, we use bitwise AND operation. Bitwise AND operation between byte 10(.btn) and button bit will return 1 once the button is pressed, otherwise it is 0. Button states are stored in private variables: .btnA, .btnB, .btnX, .btnY. .btnA = .btn & 1; .btnB = .btn & 2; .btnX = .btn & 4; .btnY = .btn & 8; ; Parse USB data. Received data is stored in array. .arg2 argument holds array handle. ; According to the 'table', byte 9 represents throttle buttons. We store it in private variable .th. .th = array_getdata(.arg2, 9); ; Left throttle button value is stored in private variable .lth. ; Right throttle button value is stored in private variable .rth. ; Both throttle button values are set to zero until any throttle button is pressed. .lth = 0; .rth = 0; ;If throttle buttons are pressed, .rth and .lth values are calculated in range [-1..1] if(.th < 0x80, .rth = .th / 0x80); if(.th > 0x80, .lth = (.th - 0x80) / 0x7F); ;Sets .ax and .ay values to zero .ax = 0; .ay = 0; ; With ABXY we define direction of jogging, with throttle we define speed of jogging. ; If ABXY button(s) are pressed, and left throttle button is pressed, .ax and .ay values are calculated in range [-1..1]. if(.btnX, .ax = .ax - .lth); if(.btnB, .ax = .ax + .lth); if(.btnA, .ay = .ay - .lth); if(.btnY, .ay = .ay + .lth); jog(0, .ax, .ay, 0);