Using Adafruit Macropad as LinuxCNC Control Pendant

Update, 2021-09-25: For compatibility with CircuitPython 7.0.0


CircuitPython recently gained the power to have custom USB descriptors. With these, we can define a USB HID device that will work with LinuxCNC's hal_input.

For instance, the Adafruit Macropad has a (very coarse, just 20 detents/revolution) encoder, 12 keyswitch positions, and an OLED screen.

The two pieces of software below, when placed in the CIRCUITPY drive as boot.py and code.py configure it for use with hal_input, using a halcmd line similar to loadusr -W hal_input Macropad. I haven't actually done the work of hooking it all the way up to Touchy yet, but it causes all the buttons & the encoder to appear in halcmd show pin.

This is just the device I picked first; there's nothing to prevent you from hooking up more exotic things like voltage/temperature monitors through added CircuitPython code. Addition of output reports for status indicators is left for the motivated reader.

Running with python3? You may need a very recently contributed patch to make hal_input work again.

import usb_hid
PENDANT_DESCRIPTOR = bytes((
0x05, 0x01, # USAGE_PAGE (Generic Desktop)
0x09, 0x02, # USAGE (Mouse)
0xa1, 0x01, # COLLECTION (Application)
0x85, 0x04, # Report ID (4),
0x09, 0x01, # USAGE (Pointer)
0xa1, 0x00, # COLLECTION (Physical)
0x05, 0x09, # USAGE_PAGE (Button)
0x19, 0x01, # USAGE_MINIMUM (Button 1)
0x29, 0x0d, # USAGE_MAXIMUM (Button 13)
0x15, 0x00, # LOGICAL_MINIMUM (0)
0x25, 0x01, # LOGICAL_MAXIMUM (1)
0x95, 0x0d, # REPORT_COUNT (13)
0x75, 0x01, # REPORT_SIZE (1)
0x81, 0x02, # INPUT (Data,Var,Abs)
0x95, 0x01, # REPORT_COUNT (3)
0x75, 0x03, # REPORT_SIZE (1)
0x81, 0x03, # INPUT (Cnst,Var,Abs)
0x05, 0x01, # USAGE_PAGE (Generic Desktop)
0x09, 0x30, # USAGE (X)
0x15, 0x81, # LOGICAL_MINIMUM (-127)
0x25, 0x7f, # LOGICAL_MAXIMUM (127)
0x75, 0x08, # REPORT_SIZE (8)
0x95, 0x02, # REPORT_COUNT (1)
0x81, 0x06, # INPUT (Data,Var,Rel)
0xc0, # END_COLLECTION
0xc0 # END_COLLECTION
))
pendant = usb_hid.Device(
report_descriptor=PENDANT_DESCRIPTOR,
usage_page=0x01, # Generic Desktop Control
usage=0x05, # Gamepad
in_report_lengths=(3,), # This gamepad sends 3 bytes in its report.
out_report_lengths=(0,), # It does not receive any reports.
report_ids=(4,), # It does not use report IDs
)
usb_hid.enable([
pendant
]
)
view raw boot.py hosted with ❤ by GitHub
# pylint: disable=import-error, unused-import, too-few-public-methods
import displayio
import terminalio
from adafruit_display_shapes.rect import Rect
from adafruit_display_text import label
from adafruit_macropad import MacroPad
import rainbowio
import usb_hid
import time
# INITIALIZATION -----------------------
macropad = MacroPad()
macropad.display.auto_refresh = False
macropad.pixels.auto_write = False
# Set up displayio group with all the labels
group = displayio.Group()
for key_index in range(12):
x = key_index % 3
y = key_index // 3
group.append(label.Label(terminalio.FONT, text='', color=0xFFFFFF,
anchored_position=((macropad.display.width - 1) * x / 3,
macropad.display.height - 1 -
(3 - y) * 12),
anchor_point=(0.0, 1.0)))
group.append(Rect(0, 0, macropad.display.width, 12, fill=0xFFFFFF))
group.append(label.Label(terminalio.FONT, text='', color=0x000000,
anchored_position=(macropad.display.width//2, -2),
anchor_point=(0.5, 0.0)))
macropad.display.show(group)
group[0].text = "X"
group[1].text = "Y"
group[2].text = "Z"
group[3].text = ".1"
group[4].text = ".01"
group[5].text = ".001"
group[6].text = "H. All"
group[7].text = "Run"
group[8].text = "Pause"
group[9].text = "Abort"
group[10].text = "Power"
group[11].text = "E-Stop"
group[13].text = "LinuxCNC Pendant"
last_position = macropad.encoder
last_encoder_switch = macropad.encoder_switch_debounced.pressed
macropad.display.refresh()
dev = usb_hid.devices[-1]
# MAIN LOOP ----------------------------
last_report = None
report = bytearray(3)
def let_button(key_number, pressed):
idx = key_number // 8
bit = 1 << (key_number & 7)
if pressed:
report[idx] |= bit
else:
report[idx] &= ~bit
while True:
# Read encoder position. If it's changed, switch apps.
position = macropad.encoder
report[2] = (position - last_position) & 0xff
last_position = position
let_button(12, macropad.encoder_switch)
while event := macropad.keys.events.get():
let_button(event.key_number, event.pressed)
if report != last_report:
print("sending report", report)
try:
dev.send_report(report)
last_report = report[:]
except OSError as e:
print(e)
time.sleep(.01)
view raw code.py hosted with ❤ by GitHub


Entry first conceived on 18 August 2021, 19:03 UTC, last modified on 25 September 2021, 16:23 UTC
Website Copyright © 2004-2024 Jeff Epler