Skip to content

Instantly share code, notes, and snippets.

@blatayue
Last active August 5, 2024 06:42
Show Gist options
  • Save blatayue/57165ddad2402ba7e708dcd7606975a3 to your computer and use it in GitHub Desktop.
Save blatayue/57165ddad2402ba7e708dcd7606975a3 to your computer and use it in GitHub Desktop.
Superbird MIDI Gadget

Superbird MIDI Gadget

Modifications I made to the scripts in /scripts/in the superbird-kiosk-app (v1.8.0) to create a MIDI USB gadget, the RNDIS gadget is also still enabled

Replace corresponding scripts to have your Car Thing send MIDI NoteOn/NoteOff events on hardware button presses

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)
#!/bin/bash
# Setup Linux USB Gadget
# this version is meant to run within native debian on the device
# Available options:
# usb_f_rndis.ko
# usb_f_fs.ko
# usb_f_midi.ko
# usb_f_mtp.ko
# usb_f_ptp.ko
# usb_f_audio_source.ko
# usb_f_accessory.ko
######### Variables
USBNET_PREFIX="192.168.7"
SERIAL_NUMBER="12345678"
# 18d1:4e40 Google Inc. Nexus 7
ID_VENDOR="0x18d1"
ID_PRODUCT="0x4e40"
MANUFACTURER="Spotify"
PRODUCT="Superbird"
# Research
# starting point: https://github.com/frederic/superbird-bulkcmd/blob/main/scripts/enable-adb.sh.client
# info about configfs https://elinux.org/images/e/ef/USB_Gadget_Configfs_API_0.pdf
# info about usbnet and bridging https://developer.ridgerun.com/wiki/index.php/How_to_use_USB_device_networking
# more info, including for windows https://learn.adafruit.com/turning-your-raspberry-pi-zero-into-a-usb-gadget/ethernet-gadget
# a gist that was helpful: https://gist.github.com/geekman/5bdb5abdc9ec6ac91d5646de0c0c60c4
# https://www.kernel.org/doc/Documentation/usb/gadget_configfs.txt
######### Functions
create_device() {
# create usb gadget device
ID_VEND="$1"
ID_PROD="$2"
BCD_DEVICE="$3"
BCD_USB="$4"
echo "### Creating device $ID_VEND $ID_PROD"
mkdir -p "/dev/usb-ffs"
mkdir -p "/dev/usb-ffs/adb"
mountpoint /sys/kernel/config/ || mount -t configfs none "/sys/kernel/config/"
mkdir -p "/sys/kernel/config/usb_gadget/g1"
echo "$ID_VEND" > "/sys/kernel/config/usb_gadget/g1/idVendor"
echo "$ID_PROD" > "/sys/kernel/config/usb_gadget/g1/idProduct"
echo "$BCD_DEVICE" > "/sys/kernel/config/usb_gadget/g1/bcdDevice"
echo "$BCD_USB" > "/sys/kernel/config/usb_gadget/g1/bcdUSB"
mkdir -p "/sys/kernel/config/usb_gadget/g1/strings/0x409"
sleep 1
}
configure_device() {
# configure usb gadget device
MANUF="$1"
PROD="$2"
SERIAL="$3"
CONFIG_NAME="$4"
echo "### Configuring device as $MANUF $PROD"
echo "$MANUF" > "/sys/kernel/config/usb_gadget/g1/strings/0x409/manufacturer"
echo "$PROD" > "/sys/kernel/config/usb_gadget/g1/strings/0x409/product"
echo "$SERIAL" > "/sys/kernel/config/usb_gadget/g1/strings/0x409/serialnumber"
mkdir -p "/sys/kernel/config/usb_gadget/g1/configs/c.1"
mkdir -p "/sys/kernel/config/usb_gadget/g1/configs/c.1/strings/0x409"
echo "$CONFIG_NAME" > "/sys/kernel/config/usb_gadget/g1/configs/c.1/strings/0x409/configuration"
echo 500 > "/sys/kernel/config/usb_gadget/g1/configs/c.1/MaxPower"
ln -s "/sys/kernel/config/usb_gadget/g1/configs/c.1" "/sys/kernel/config/usb_gadget/g1/os_desc/c.1"
sleep 1
}
add_function(){
# add a function to existing config id
FUNCTION_NAME="$1"
echo "### adding function $FUNCTION_NAME to config c.1"
mkdir -p "/sys/kernel/config/usb_gadget/g1/functions/${FUNCTION_NAME}"
ln -s "/sys/kernel/config/usb_gadget/g1/functions/${FUNCTION_NAME}" "/sys/kernel/config/usb_gadget/g1/configs/c.1"
}
attach_driver(){
# attach the created gadget device to our UDC driver
UDC_DEVICE=$(/bin/ls -1 /sys/class/udc/) # ff400000.dwc2_a
echo "### Attaching gadget to UDC device: $UDC_DEVICE"
echo "$UDC_DEVICE" > /sys/kernel/config/usb_gadget/g1/UDC
sleep 1
}
configure_usbnet() {
DEVICE="$1"
NETWORK="$2" # just the first 3 octets
NETMASK="$3"
echo "### bringing up $DEVICE with ${NETWORK}.2"
ifconfig "$DEVICE" up
ifconfig "$DEVICE" "${NETWORK}.2" netmask "$NETMASK" broadcast "${NETWORK}.255"
echo "adding routes for $DEVICE"
ip route add default via "${NETWORK}.1" dev "$DEVICE"
echo "making sure you have a dns server"
echo "nameserver 1.1.1.1" > /etc/resolv.conf
sleep 1
}
shutdown_gadget() {
# shutdown and clean up usb gadget and services
# ref: https://wiki.tizen.org/USB/Linux_USB_Layers/Configfs_Composite_Gadget/Usage_eq._to_g_ffs.ko
echo "$UDC_DEVICE" > /sys/kernel/config/usb_gadget/g1/UDC
find "/sys/kernel/config/usb_gadget/g1/configs/c.1" -type l -exec unlink {} \;
rm -r "/sys/kernel/config/usb_gadget/g1/configs/c.1/strings/0x409"
rm -r /sys/kernel/config/usb_gadget/g1/strings/0x409
rm -r "/sys/kernel/config/usb_gadget/g1/configs/c.1"
rm -r /sys/kernel/config/usb_gadget/g1/functions/*
rm -r /sys/kernel/config/usb_gadget/g1/
}
######### Entrypoint
echo "### Configuring USB Gadget"
create_device "$ID_VENDOR" "$ID_PRODUCT" "0x0223" "0x0200"
configure_device "$MANUFACTURER" "$PRODUCT" "$SERIAL_NUMBER" "Multi-Function Device"
add_function "rndis.usb0"
# midi
mkdir -p /sys/kernel/config/usb_gadget/g1/functions/midi.usb0
echo "Superbird MIDI" > /sys/kernel/config/usb_gadget/g1/functions/midi.usb0/id
ln -s /sys/kernel/config/usb_gadget/g1/functions/midi.usb0 /sys/kernel/config/usb_gadget/g1/configs/c.1
attach_driver
configure_usbnet "usb0" "$USBNET_PREFIX" "255.255.255.0"
echo "Done setting up USB Gadget"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment