title: Qt Key Sender tags: osc python This is a simple GUI app that forwards keypresses via OSC. My main use is to bind to actions in Reaper. I connect a keyboard to a spare machine, and run this app, and keypresses turn into OSC messages sent to Reaper, which can be bound to actions. (Enable OSC in control surfaces in preferences, and enable binding messages to actions.) It needs Python modules `PySide6` and `python-osc` installed. This uses the convenient `from PySide6... import *` thing, but that causes issues with more complex apps whose source is split into multiple files. ## The Code ```py #!/usr/bin/env python from PySide6.QtCore import * from PySide6.QtGui import * from PySide6.QtWidgets import * from PySide6.QtNetwork import * from pythonosc import udp_client from pythonosc.osc_message import OscMessage import sys,os,socket,re import argparse default_host = "localhost" default_port = 9000 try: keysend_port = int(os.getenv("KEYSEND_PORT",9000)) except ValueError: print("Invalid KEYSEND_PORT -- using default") keysend_port = default_port def get_ip_for_host(host,family=socket.AF_INET): try: addrinfo = socket.getaddrinfo(host,0,family=family,type=socket.SOCK_DGRAM) if len(addrinfo) == 0: print(f"No info for {host}",file=sys.stderr) return None return addrinfo[0][4][0] except socket.gaierror as e: print(f"getaddrinfo failed for {host} : {e}",file=sys.stderr) return None class Config: def __init__(self): self.host = os.getenv("KEYSEND_HOST","localhost") try: self.port = int(os.getenv("KEYSEND_PORT",9000)) except ValueError: self.port = 9000 print("Invalid KEYSEND_PORT -- default value used:",self.port) self.hostname = socket.gethostname() self.initial_channel = os.getenv("KEYSEND_CHANNEL","f1") config = Config() class SendKey: def __init__(self,host,port,chan=999): self.host = host self.port = port self.ip = get_ip_for_host(self.host) self.client = udp_client.SimpleUDPClient(self.ip,self.port) self.setChannel(chan) self.delegates = {} def send(self,k): if self.chan in self.delegates: return self.delegates[self.chan].send(self,k) addr = f"/hh/{self.chan}/key/{k}" print("send",addr) self.client.send_message(addr,[]) def send_message(self,addr,*args): self.client.send_message(addr,args) def setTarget(self,ip,port=None): self.ip = ip if port is not None: self.port = port print(f"New target {ip}:{port}") self.client = udp_client.SimpleUDPClient(self.ip,self.port) def setChannel(self,chan): chan = str(chan).replace("/","") if len(chan) == 0: return self.chan = chan print(f"Channel now {chan}") # load from config.json, or ENV, not command line args class Win(QWidget): def __init__(self,config=config): super().__init__() self.sendkey = SendKey(config.host,config.port,chan=config.initial_channel) self.resize(640,400) self.channel = config.initial_channel self.last = "" self.history_length = 5 self.lasts = [] def paintEvent(self,e): with QPainter(self) as p: rect = self.rect() p.fillRect(rect,QColor("black")) p.setPen(QColor("#f7f")) font = QFont("Optima",36) p.setFont(font) p.drawText(QPoint(300,100),f"Ch: {self.channel}") p.setPen(QColor("#ff7")) p.drawText(QPoint(250,200),f"{self.last}") p.setPen(QColor("#77f")) p.setFont(QFont("Optima",18)) p.drawText(QPoint(10,300),f"{' '.join(self.lasts)}") def keyPressEvent(self,event): sendkey = self.sendkey key = event.key() key = QKeySequence(key).toString().lower() print("key",key) if len(key) == 0: return if not key in "abcdefghijklmnopqrstuvwxyz0123456789" and\ not key in ["left","right","up","down"] and\ not key in ["home","end","pgup","pgdown"] and\ not key in ["ins","del","esc","space"] and\ not (key[0] == "f" and key[1:].isnumeric()) and\ not key in "!\"£$%^&*()_+-=[]{};:'#@~,./<>?\\|`¬": return modifiers = event.modifiers() if modifiers & Qt.ShiftModifier: key = "S-"+key if modifiers & Qt.MetaModifier: key = "M-"+key if modifiers & Qt.ControlModifier: key = "C-"+key if modifiers & Qt.AltModifier: key = "A-"+key if key == "A-C-q": app.quit() if re.match(r"[ACSM-]*f\d+$",key): self.channel = key self.update() return sendkey.setChannel(key) print(f"Key: {key} to {sendkey.chan}") self.last = key self.lasts.append(key) self.lasts = self.lasts[-self.history_length:] sendkey.send(key) self.update() def main(): parser = argparse.ArgumentParser(prog="kb2osc", description="Keys to OSC sender",add_help=False) parser.add_argument("--help",action="help") parser.add_argument("-h","--host",required=False) parser.add_argument("-p","--port",type=int,required=False) parser.add_argument("-c","--channel",help="Initial channel",required=False) ns = parser.parse_args() if ns.channel is not None: config.initial_channel = ns.channel if ns.host is not None: config.host = ns.host if ns.port is not None: config.port = ns.port hostname = socket.gethostname() app = QApplication([]) win = Win() win.show() app.exec() if __name__ == "__main__": main() ```