|
import time |
|
import struct |
|
import logging |
|
|
|
from threading import Thread |
|
from threading import Event as ThreadEvent |
|
|
|
import os |
|
|
|
# Modified from https://github.com/bishopdynamics/superbird-debian-kiosk/blob/main/files/data/scripts/buttons_app.py |
|
# by shgiraffe to read buttons and send midi events over usb gadget |
|
|
|
# All the device buttons are part of event0, which appears as a keyboard |
|
# buttons along the edge are: 1, 2, 3, 4, m |
|
# next to the knob: ESC |
|
# knob click: Enter |
|
# Turning the knob is a separate device, event1, which also appears as a keyboard |
|
# turning the knob corresponds to the left and right arrow keys |
|
|
|
|
|
DEV_BUTTONS = '/dev/input/event0' |
|
DEV_KNOB = '/dev/input/event1' |
|
|
|
time.sleep(5) # wait for gadget to create midi device |
|
MIDI_DEVICE = os.open('/dev/snd/midiC1D0', os.O_WRONLY) |
|
|
|
# for event0, these are the keycodes for buttons |
|
BUTTONS_CODE_MAP = { |
|
2: '1', |
|
3: '2', |
|
4: '3', |
|
5: '4', |
|
50: 'm', |
|
28: 'ENTER', |
|
1: 'ESC', |
|
} |
|
# for event1, when the knob is turned it is always keycode 6, but value changes on direction |
|
KNOB_LEFT = 4294967295 # actually -1 but unsigned int so wraps around |
|
KNOB_RIGHT = 1 |
|
# https://github.com/torvalds/linux/blob/v5.5-rc5/include/uapi/linux/input.h#L28 |
|
# long int, long int, unsigned short, unsigned short, unsigned int |
|
EVENT_FORMAT = 'llHHI' |
|
EVENT_SIZE = struct.calcsize(EVENT_FORMAT) |
|
|
|
logformat = logging.Formatter( |
|
'%(created)f %(levelname)s [%(filename)s:%(lineno)d]: %(message)s') |
|
logger = logging.getLogger('buttons') |
|
logger.setLevel(logging.DEBUG) |
|
|
|
fh = logging.FileHandler('/var/log/buttons.log') |
|
fh.setLevel(logging.DEBUG) |
|
fh.setFormatter(logformat) |
|
logger.addHandler(fh) |
|
|
|
ch = logging.StreamHandler() |
|
ch.setLevel(logging.DEBUG) |
|
ch.setFormatter(logformat) |
|
logger.addHandler(ch) |
|
|
|
|
|
def translate_event(etype: int, code: int, value: int) -> tuple[str, int]: |
|
""" |
|
Translate combination of type, code, value into string representing button pressed and the current value/state |
|
""" |
|
if etype == 1: |
|
# button press |
|
if code in BUTTONS_CODE_MAP: |
|
# value is 1 for down 0 for up |
|
return BUTTONS_CODE_MAP[code], value |
|
if etype == 2 and code == 6: |
|
# knob turn |
|
if value == KNOB_RIGHT: |
|
return 'RIGHT', None |
|
if value == KNOB_LEFT: |
|
return 'LEFT', None |
|
return 'UNKNOWN' |
|
|
|
|
|
def handle_button(pressed_key: str, val: int): |
|
""" |
|
Decide what to do in response to a button press |
|
Write midi notes directly to midi out device |
|
""" |
|
# NoteOn = 0x90 |
|
# NoteOff = 0x80 |
|
# where low nibble is channel |
|
message_type = 0x90 if val == None or val == 1 else 0x80 |
|
|
|
# https://inspiredacoustics.com/en/MIDI_note_numbers_and_center_frequencies |
|
midi_notes = { |
|
'1': 61, '2': 62, '3': 63, '4': 64, 'm': 65, |
|
'ENTER': 66, 'ESC': 67, 'LEFT': 68, 'RIGHT': 69 |
|
} |
|
|
|
# Write NoteOn/NoteOff event |
|
os.write(MIDI_DEVICE, bytes([message_type, midi_notes[pressed_key], 64])) |
|
|
|
# Also send NoteOff for knob |
|
if pressed_key in ["LEFT", "RIGHT"]: |
|
os.write(MIDI_DEVICE, bytes([0x80, midi_notes[pressed_key], 64])) |
|
|
|
|
|
|
|
class EventListener(): |
|
""" |
|
Listen to a specific /dev/eventX and call handle_button |
|
""" |
|
|
|
def __init__(self, device: str) -> None: |
|
self.device = device |
|
self.stopper = ThreadEvent() |
|
self.thread: Thread = None |
|
self.start() |
|
|
|
def start(self): |
|
""" |
|
Start listening thread |
|
""" |
|
logger.info(f'Starting listener for {self.device}') |
|
self.thread = Thread(target=self.listen, daemon=True) |
|
self.thread.start() |
|
|
|
def stop(self): |
|
""" |
|
Stop listening thread |
|
""" |
|
logger.info(f'Stopping listener for {self.device}') |
|
self.stopper.set() |
|
self.thread.join() |
|
|
|
def listen(self): |
|
|
|
|
|
""" |
|
To run in thread, listen for events and call handle_buttons if applicable |
|
""" |
|
with open(self.device, "rb") as in_file: |
|
event = in_file.read(EVENT_SIZE) |
|
|
|
while event and not self.stopper.is_set(): |
|
if self.stopper.is_set(): |
|
break |
|
(_sec, _usec, etype, code, value) = struct.unpack( |
|
EVENT_FORMAT, event) |
|
# logger.info(f'Event: type: {etype}, code: {code}, value:{value}') |
|
event = translate_event(etype, code, value) |
|
if event[0] in ['1', '2', '3', '4', 'm', 'ENTER', 'ESC', 'LEFT', 'RIGHT']: |
|
handle_button(event[0], event[1]) |
|
event = in_file.read(EVENT_SIZE) |
|
|
|
|
|
if __name__ == '__main__': |
|
logger.info('Starting buttons listeners') |
|
EventListener(DEV_BUTTONS) |
|
EventListener(DEV_KNOB) |
|
|
|
while True: |
|
time.sleep(1) |