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 | |
] | |
) |
# 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) |
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