#!/usr/bin/python ########################################################### ####### A LINUX DRIVER FOR THE NINTENDO WII REMOTE ###### ##********************************************************* ## ___ ___ ## /\ \ /\ \ _____ ## _\:\ \ |::\ \ /::\ \ ## /\ \:\ \ |:|:\ \ /:/\:\ \ ## _\:\ \:\ \ __|:|\:\ \ /:/ \:\__\ ## /\ \:\ \:\__\ /::::|_\:\__\ /:/__/ \:|__| ## \:\ \:\/:/ / \:\~~\ \/__/ \:\ \ /:/ / ## \:\ \::/ / \:\ \ \:\ /:/ / ## \:\/:/ / \:\ \ \:\/:/ / ## \::/ / \:\__\ \::/ / ## \/__/ \/__/ \/__/ ## ## WMD v0.0.3.99999: Wiiwanna Motte Dammiit! ## by Francois A. Bradet, aka e-Hernick#wiili@freenode ## coded from 2006-12-04 to 2006-12-08 19:43 EDT ## with code by Micah Dowty and Marcan ## with algorithms by Ian Rickard ## share use modify as you wish; CC-PD+GPL2 ## _ _ _____ _ _ ## | | | | | __ \ | | | | ## | | | | | |__) | ___ _ __ | |_| |__ ___ __ ____ _ _ _ ## | | | | | _ / / _ \| '_ \ | __| '_ \ / _ \ \ \ /\ / / _` | | | | ## | |__| | | | \ \ | (_) | | | | | |_| | | | __/ \ V V / (_| | |_| | ## \____/ |_| \_\ \___/|_| |_| \__|_| |_|\___| \_/\_/ \__,_|\__, | ## __/ | ## |___/ ## _ _ _ _ _ _ ## | | | | | | | | (_) | | ## | |_ ___ __| | ___ ___| |_ _ __ _ _ ___| |_ _ ___ _ __ | | ## | __/ _ \ / _` |/ _ \/ __| __| '__| | | |/ __| __| |/ _ \| '_ \| | ## | || (_) | | (_| | __/\__ \ |_| | | |_| | (__| |_| | (_) | | | |_| ## \__\___/ \__,_|\___||___/\__|_| \__,_|\___|\__|_|\___/|_| |_(_) ## ############################################################ ####### I. INTRODUCTION: READ ME FIRST OR ELSE ######### ##********************************************************** ## It is important that you read this section before ## trying to use WMD, or else you will fail miserably. ## ## WMD is a Linux Driver for the Nintendo Wii Remote ## ## WMD lets you use the Wiimote as a mouse ## WMD lets you use the Wiimote as a keyboard ## WMD lets you use the Wiimote as a joystick ## WMD lets you use the Wiimote as a vibrator ## ## WMD is dangerous and experimental ## WMD could make your Wii explode! ## WMD has no warranty other than the CC-PD+GPL2 ## ## You need technical knowledge and Linux experience ## if you are to succeed in installing WMD without help. ## ## But more than that, you need to install the following: ## ## ** BlueZ (MUST!) ## ## ** PyBluez 0.9.1 (MUST!) # aka python-bluez? on ubuntu ## http://org.csail.mit.edu/pybluez/release/pybluez-src-0.9.1.tar.gz ## ## To use Xlib event mode (GOOD) ## ++ python-xlib 0.12 ## ++ with buffer overflow patch (READ SECTION: XLIB) ## ## To use uinput event mode (GOOD) ## ++ Linux 2.6 kernel with evdev and uinput modules ## ## To use uinput events to move the mouse (OK) ## ++ Xorg 7.0 with evdev ## ############################################################ ####### II. BASIC CONFIGURATION: DO IT NOW ######### ##********************************************************** ## ## This is the address of my Wiimote. ## MY_WIIMOTE_ADDR="00:19:1D:25:16:43" ## ## Or, if I'm lazy, I'll just comment it out and uncomment ## An empty address: it will force autodetection: ## #MY_WIIMOTE_ADDR="" ## ## ## Next, I will choose which IO Modes I want to use ## for sending keypresses, clicks, mouse movements and gestures ## IO_MODES = { # SET TO TRUE IF YOU HAVE: 'UINPUT': True, ## the uinput kernel module loaded. 'XLIB': True, ## python-xlib with the buffer overflow patch. 'X_EVDEV': False, ## evdev_drv and a customized xorg.conf. 'PYOSD': True } ## ## ## If I'm using IO_MODES['UINPUT'] I'll need to ## Set the path of my uinput device ## UINPUT_DEV = "/dev/misc/uinput" #UINPUT_DEV = "/dev/input/uinput" ##ubuntu - you need to modprobe uinput first #UINPUT_DEV = "/dev/uinput" ## ## I'll also check that I have the right UNIX rights for it ## ## ## If I changed the default IO modes ## I'll need to choose new IO channels for them ## Changing the default IO channels is risky and poorly tested ## The only channel I'd consider changing for now is 'EV_ABS' to 'X_EVDEV' ## IO_CHANNELS = { 'EV_ABS': 'XLIB', 'EV_KEY': 'UINPUT', 'EV_REL': 'UINPUT' } ## ## ## Now, this is the fun part, when you can assign ## Actions to Buttons ## commandMap = { 'A': ['click', 'BTN_RIGHT' ], # A key: right mouse button click 'B': ['click', 'BTN_LEFT' ], # B key: left mouse button click 'H': ['quit'], # Home key: quits WMD '-': ['vibrate', 'toggle' ], # Minus key: starts vibrator toggle '+': ['vibrate', 'off' ], # Plus key: stops vibrator and resets IR # '1': ['guess', 'status' ], '2': ['guess', 'orien' ], # 2 key: guess orientation of the Wiimote '1': ['key', 'KEY_1' ], # 1 key: press the '1' key like a keyboard # '2': ['key', 'KEY_2' ], 'U': ['led', 'toggle', 1 ], # Toggles LED 1 'D': ['led', 'toggle', 2 ], # LED 2 'L': ['led', 'toggle', 3 ], # LED 3 'R': ['led', 'toggle', 4 ] # LED 4 } ## ## ## This will enable IR sensing by default IR_ENABLE = 1 ## ## ## If you having trouble reaching the edges of your screen with the IR mouse ## You want to raise the Dead-Zone values at the most 0.50 XDZ = 0.35 YDZ = 0.25 ## ## ## If for some reason WMD isn't detecting your screen size correctly ## Set these to the right values. If 0, WMD will autodetect. SCREEN_WIDTH = 1600 SCREEN_HEIGHT = 0 ## ## ## These values influence motion sensing in unknown ways ## force_log_maxlen = 10 force_disturbance = 200 quash = { 'duration': 5, 'age': 0 } HYST = 5 ## ## ## These influence IR pointer behaviour in unknown ways dots_fifo_len = 2 dots_quash = { 'duration': 2, 'age': 0 } ## ## VIBSM=0 ## DON'T TOUCH THOSE TWO LINES: these are constants for debug levels LOG_ERR = 1; LOG_INFO = 2; LOG_BTN = 4; LOG_FORCE = 8; LOG_IR = 16 DEBUG_FORCE = 32; DEBUG_IR = 64; DEBUG_BT_SEND = 128; DEBUG_BT_RECV = 256 ## ## ## Here you can choose how much logging you want to see LOG_LEVEL = LOG_ERR | LOG_INFO | LOG_FORCE LOG_LEVEL = LOG_ERR | LOG_INFO | LOG_FORCE #| LOG_BTN | LOG_IR #more stuff #LOG_LEVEL = LOG_ERR | LOG_INFO # minimal ############### ## SECTION XLIB ############### ## ## You must used a PATCHED python-xlib 0.12 because the original ## has a terrible bug that prevents it from starting with modern Xorg ## python-xlib hasn't been maintained in years, but the patch is simple. ## ## First you install python-xlib 0.12 ## ## Then you find the file protocol/display.py #### locate protocol/display.py | grep -v pyc ## /usr/lib/python2.4/site-packages/Xlib/protocol/display.py ## ## ## Then you check if it's already patched #### grep self.socket.recv /usr/lib/python2.4/site-packages/Xlib/protocol/display.py ## recv = self.socket.recv(4096) ## ## If the value is 4096, all is good ## If the value is 2048, you need to edit the file and change the value to 4096 ## ## It's THAT EASY! ## ## On Gentoo, dev-python/python-xlib-0.12-r2 is already patched. ## ################ #### #### PREFLIGHT CHECK - IF YOU'RE HAVING ANY TROUBLE #### RUN THE FOLLOWING COMMANDS AND COMPARE THE OUTPUT #### YOU GET TO THE OUTPUT I GOT ## #### gzcat /proc/config.gz | grep -iE "evdev|uinput" ## CONFIG_INPUT_EVDEV=y ## CONFIG_INPUT_UINPUT=m ## #modprobe uinput #modprobe evdev #### lsmod | grep uinput ## uinput 8832 0 ## #### find /dev -name uinput ## /dev/uinput ## /dev/misc/uinput ## #### locate evdev_drv ## /usr/lib/xorg/modules/input/evdev_drv.so ## #### locate bluetooth.py ## /usr/lib/python2.4/site-packages/bluetooth.py ## #### grep PyBluez /usr/lib/python2.4/site-packages/bluetooth.py | wc -l ## 2 ## #### hcitool scan | grep Nintendo ## 00:19:1D:25:16:43 Nintendo RVL-CNT-01 ## ## If you want do use XEVDEV IO, you need to install evdev_drv for Xorg ## You also need to add this section to your xorg.conf # # Section "InputDevice" # Identifier "Wiimote" # Driver "evdev" # Option "Name" "Nintendo Wiimote" # EndSection # ## You will also need to modify your ServerLayout section. ## You must have only one CorePointer, but you can have ## many AlwaysCore devices. You don't need a Synaptics ## touchpad, I just have one on my laptop. ## So this is what the beginning of my xorg.conf looks like: # # Section "ServerLayout" # Identifier "X.org Configured" # Screen 0 "Screen0" 0 0 # InputDevice "Mouse0" "CorePointer" # InputDevice "Keyboard0" "CoreKeyboard" # InputDevice "Synaptics Touchpad" "AlwaysCore" # InputDevice "Wiimote" "AlwaysCore" # EndSection # ## Scaling is known to work badly with evdev_drv # We import PyBluez and other stuff from bluetooth import * from copy import copy import string, pprint import os, struct, fcntl, time, math, string, random pp = pprint.PrettyPrinter(indent=4) # This is what the Wiimote calls itself (Bluetooth Name) WIIMOTE_NAME = "Nintendo RVL-CNT-01" status = { 'go':1 } socket = {} force_log = [] dots_fifo = [] # These are the button mappings buttonmap = { '2': 0x0001, '1': 0x0002, 'B': 0x0004, 'A': 0x0008, '-': 0x0010, 'H': 0x0080, 'L': 0x0100, 'R': 0x0200, 'D': 0x0400, 'U': 0x0800, '+': 0x1000 } buttonstates = {} oldbuttonstates = {} for bt in buttonmap: buttonstates[bt] = 0 oldbuttonstates[bt] = 0 led_state = [0,0,0,0] # These are commands for the wiimote commandcodes = { "vibrate_on": 0x521301, "vibrate_off": 0x521300, # "forcerep_on": 0x52120431, # "forcerep_off": 0x52120030, "leds_off": 0x521100 } # These are the Wiimote control codes FEATURE_DISABLE = 0x00 FEATURE_ENABLE = 0x04 IR_MODE_OFF = 0 IR_MODE_STD = 1 IR_MODE_EXP = 3 IR_MODE_FULL = 5 CMD_SET_REPORT = 0x52 RID_LEDS = 0x11 RID_MODE = 0x12 RID_IR_EN = 0x13 RID_SPK_EN = 0x14 RID_STATUS = 0x15 RID_WMEM = 0x16 RID_RMEM = 0x17 RID_SPK = 0x18 RID_SPK_MUTE = 0x19 RID_IR_EN2 = 0x1A MODE_BASIC = 0x30 MODE_ACC = 0x31 MODE_ACC_IR = 0x33 MODE_FULL = 0x3e # These are uinput control codes UI_DEV_CREATE = 0x5501 UI_DEV_DESTROY = 0x5502 UI_SET_EVBIT = 0x40045564 UI_SET_KEYBIT = 0x40045565 UI_SET_RELBIT = 0x40045566 UI_SET_ABSBIT = 0x40045567 EV_SYN = 0x00 EV_KEY = 0x01 EV_REL = 0x02 EV_ABS = 0x03 REL_X = 0x00 REL_Y = 0x01 ABS_X = 0x00 ABS_Y = 0x01 BUS_USB = 0x03 BTN_MOUSE = 0x110 BTN_TOUCH = 0x14a BTN_TOOL_FINGER = 0x145 SYN_REPORT = 0 class EnumDict: """A 1:1 mapping from numbers to strings or other objects, for enumerated types and other assigned numbers. The mapping can be queried in either direction. All values, by default, map to themselves. """ def __init__(self, numberMap): self.numberMap = numberMap self.nameMap = {} for key, value in numberMap.iteritems(): self.nameMap[value] = key def toNumber(self, name): return self.nameMap.get(name, name) def fromNumber(self, num): return self.numberMap.get(num, num) typeMap = EnumDict({ 0x00: "EV_RST", 0x01: "EV_KEY", 0x02: "EV_REL", 0x03: "EV_ABS", 0x04: "EV_MSC", 0x11: "EV_LED", 0x12: "EV_SND", 0x14: "EV_REP", 0x15: "EV_FF", }) ## ************** ATTENTION USER **************** # _ __________ ____ __ _____ # | |/ / ____\ \ / / \/ | /\ | __ \ # | ' /| |__ \ \_/ /| \ / | / \ | |__) | # | < | __| \ / | |\/| | / /\ \ | ___/ # | . \| |____ | | | | | |/ ____ \| | # |_|\_\______| |_| |_| |_/_/ \_\_| ## THIS IS THE KEYMAP ## USE THESE KEY_CODES IN THE commandMap codeMaps = { "EV_KEY": EnumDict({ 0: "KEY_RESERVED", 1: "KEY_ESC", 2: "KEY_1", 3: "KEY_2", 4: "KEY_3", 5: "KEY_4", 6: "KEY_5", 7: "KEY_6", 8: "KEY_7", 9: "KEY_8", 10: "KEY_9", 11: "KEY_0", 12: "KEY_MINUS", 13: "KEY_EQUAL", 14: "KEY_BACKSPACE", 15: "KEY_TAB", 16: "KEY_Q", 17: "KEY_W", 18: "KEY_E", 19: "KEY_R", 20: "KEY_T", 21: "KEY_Y", 22: "KEY_U", 23: "KEY_I", 24: "KEY_O", 25: "KEY_P", 26: "KEY_LEFTBRACE", 27: "KEY_RIGHTBRACE", 28: "KEY_ENTER", 29: "KEY_LEFTCTRL", 30: "KEY_A", 31: "KEY_S", 32: "KEY_D", 33: "KEY_F", 34: "KEY_G", 35: "KEY_H", 36: "KEY_J", 37: "KEY_K", 38: "KEY_L", 39: "KEY_SEMICOLON", 40: "KEY_APOSTROPHE", 41: "KEY_GRAVE", 42: "KEY_LEFTSHIFT", 43: "KEY_BACKSLASH", 44: "KEY_Z", 45: "KEY_X", 46: "KEY_C", 47: "KEY_V", 48: "KEY_B", 49: "KEY_N", 50: "KEY_M", 51: "KEY_COMMA", 52: "KEY_DOT", 53: "KEY_SLASH", 54: "KEY_RIGHTSHIFT", 55: "KEY_KPASTERISK", 56: "KEY_LEFTALT", 57: "KEY_SPACE", 58: "KEY_CAPSLOCK", 59: "KEY_F1", 60: "KEY_F2", 61: "KEY_F3", 62: "KEY_F4", 63: "KEY_F5", 64: "KEY_F6", 65: "KEY_F7", 66: "KEY_F8", 67: "KEY_F9", 68: "KEY_F10", 69: "KEY_NUMLOCK", 70: "KEY_SCROLLLOCK", 71: "KEY_KP7", 72: "KEY_KP8", 73: "KEY_KP9", 74: "KEY_KPMINUS", 75: "KEY_KP4", 76: "KEY_KP5", 77: "KEY_KP6", 78: "KEY_KPPLUS", 79: "KEY_KP1", 80: "KEY_KP2", 81: "KEY_KP3", 82: "KEY_KP0", 83: "KEY_KPDOT", 84: "KEY_103RD", 85: "KEY_F13", 86: "KEY_102ND", 87: "KEY_F11", 88: "KEY_F12", 89: "KEY_F14", 90: "KEY_F15", 91: "KEY_F16", 92: "KEY_F17", 93: "KEY_F18", 94: "KEY_F19", 95: "KEY_F20", 96: "KEY_KPENTER", 97: "KEY_RIGHTCTRL", 98: "KEY_KPSLASH", 99: "KEY_SYSRQ", 100: "KEY_RIGHTALT", 101: "KEY_LINEFEED", 102: "KEY_HOME", 103: "KEY_UP", 104: "KEY_PAGEUP", 105: "KEY_LEFT", 106: "KEY_RIGHT", 107: "KEY_END", 108: "KEY_DOWN", 109: "KEY_PAGEDOWN", 110: "KEY_INSERT", 111: "KEY_DELETE", 112: "KEY_MACRO", 113: "KEY_MUTE", 114: "KEY_VOLUMEDOWN", 115: "KEY_VOLUMEUP", 116: "KEY_POWER", 117: "KEY_KPEQUAL", 118: "KEY_KPPLUSMINUS", 119: "KEY_PAUSE", 120: "KEY_F21", 121: "KEY_F22", 122: "KEY_F23", 123: "KEY_F24", 124: "KEY_KPCOMMA", 125: "KEY_LEFTMETA", 126: "KEY_RIGHTMETA", 127: "KEY_COMPOSE", 128: "KEY_STOP", 129: "KEY_AGAIN", 130: "KEY_PROPS", 131: "KEY_UNDO", 132: "KEY_FRONT", 133: "KEY_COPY", 134: "KEY_OPEN", 135: "KEY_PASTE", 136: "KEY_FIND", 137: "KEY_CUT", 138: "KEY_HELP", 139: "KEY_MENU", 140: "KEY_CALC", 141: "KEY_SETUP", 142: "KEY_SLEEP", 143: "KEY_WAKEUP", 144: "KEY_FILE", 145: "KEY_SENDFILE", 146: "KEY_DELETEFILE", 147: "KEY_XFER", 148: "KEY_PROG1", 149: "KEY_PROG2", 150: "KEY_WWW", 151: "KEY_MSDOS", 152: "KEY_COFFEE", 153: "KEY_DIRECTION", 154: "KEY_CYCLEWINDOWS", 155: "KEY_MAIL", 156: "KEY_BOOKMARKS", 157: "KEY_COMPUTER", 158: "KEY_BACK", 159: "KEY_FORWARD", 160: "KEY_CLOSECD", 161: "KEY_EJECTCD", 162: "KEY_EJECTCLOSECD", 163: "KEY_NEXTSONG", 164: "KEY_PLAYPAUSE", 165: "KEY_PREVIOUSSONG", 166: "KEY_STOPCD", 167: "KEY_RECORD", 168: "KEY_REWIND", 169: "KEY_PHONE", 170: "KEY_ISO", 171: "KEY_CONFIG", 172: "KEY_HOMEPAGE", 173: "KEY_REFRESH", 174: "KEY_EXIT", 175: "KEY_MOVE", 176: "KEY_EDIT", 177: "KEY_SCROLLUP", 178: "KEY_SCROLLDOWN", 179: "KEY_KPLEFTPAREN", 180: "KEY_KPRIGHTPAREN", 181: "KEY_INTL1", 182: "KEY_INTL2", 183: "KEY_INTL3", 184: "KEY_INTL4", 185: "KEY_INTL5", 186: "KEY_INTL6", 187: "KEY_INTL7", 188: "KEY_INTL8", 189: "KEY_INTL9", 190: "KEY_LANG1", 191: "KEY_LANG2", 192: "KEY_LANG3", 193: "KEY_LANG4", 194: "KEY_LANG5", 195: "KEY_LANG6", 196: "KEY_LANG7", 197: "KEY_LANG8", 198: "KEY_LANG9", 200: "KEY_PLAYCD", 201: "KEY_PAUSECD", 202: "KEY_PROG3", 203: "KEY_PROG4", 205: "KEY_SUSPEND", 206: "KEY_CLOSE", 220: "KEY_UNKNOWN", 224: "KEY_BRIGHTNESSDOWN", 225: "KEY_BRIGHTNESSUP", 0x100: "BTN_0", 0x101: "BTN_1", 0x102: "BTN_2", 0x103: "BTN_3", 0x104: "BTN_4", 0x105: "BTN_5", 0x106: "BTN_6", 0x107: "BTN_7", 0x108: "BTN_8", 0x109: "BTN_9", 0x110: "BTN_LEFT", 0x111: "BTN_RIGHT", 0x112: "BTN_MIDDLE", 0x113: "BTN_SIDE", 0x114: "BTN_EXTRA", 0x115: "BTN_FORWARD", 0x116: "BTN_BACK", 0x120: "BTN_TRIGGER", 0x121: "BTN_THUMB", 0x122: "BTN_THUMB2", 0x123: "BTN_TOP", 0x124: "BTN_TOP2", 0x125: "BTN_PINKIE", 0x126: "BTN_BASE", 0x127: "BTN_BASE2", 0x128: "BTN_BASE3", 0x129: "BTN_BASE4", 0x12a: "BTN_BASE5", 0x12b: "BTN_BASE6", 0x12f: "BTN_DEAD", 0x130: "BTN_A", 0x131: "BTN_B", 0x132: "BTN_C", 0x133: "BTN_X", 0x134: "BTN_Y", 0x135: "BTN_Z", 0x136: "BTN_TL", 0x137: "BTN_TR", 0x138: "BTN_TL2", 0x139: "BTN_TR2", 0x13a: "BTN_SELECT", 0x13b: "BTN_START", 0x13c: "BTN_MODE", 0x13d: "BTN_THUMBL", 0x13e: "BTN_THUMBR", 0x140: "BTN_TOOL_PEN", 0x141: "BTN_TOOL_RUBBER", 0x142: "BTN_TOOL_BRUSH", 0x143: "BTN_TOOL_PENCIL", 0x144: "BTN_TOOL_AIRBRUSH", 0x145: "BTN_TOOL_FINGER", 0x146: "BTN_TOOL_MOUSE", 0x147: "BTN_TOOL_LENS", 0x14a: "BTN_TOUCH", 0x14b: "BTN_STYLUS", 0x14c: "BTN_STYLUS2", }), "EV_REL": EnumDict({ 0x00: "REL_X", 0x01: "REL_Y", 0x02: "REL_Z", 0x06: "REL_HWHEEL", 0x07: "REL_DIAL", 0x08: "REL_WHEEL", 0x09: "REL_MISC", }), "EV_ABS": EnumDict({ 0x00: "ABS_X", 0x01: "ABS_Y", 0x02: "ABS_Z", 0x03: "ABS_RX", 0x04: "ABS_RY", 0x05: "ABS_RZ", 0x06: "ABS_THROTTLE", 0x07: "ABS_RUDDER", 0x08: "ABS_WHEEL", 0x09: "ABS_GAS", 0x0a: "ABS_BRAKE", 0x10: "ABS_HAT0X", 0x11: "ABS_HAT0Y", 0x12: "ABS_HAT1X", 0x13: "ABS_HAT1Y", 0x14: "ABS_HAT2X", 0x15: "ABS_HAT2Y", 0x16: "ABS_HAT3X", 0x17: "ABS_HAT3Y", 0x18: "ABS_PRESSURE", 0x19: "ABS_DISTANCE", 0x1a: "ABS_TILT_X", 0x1b: "ABS_TILT_Y", 0x1c: "ABS_MISC", }), "EV_MSC": EnumDict({ 0x00: "MSC_SERIAL", 0x01: "MSC_PULSELED", }), "EV_LED": EnumDict({ 0x00: "LED_NUML", 0x01: "LED_CAPSL", 0x02: "LED_SCROLLL", 0x03: "LED_COMPOSE", 0x04: "LED_KANA", 0x05: "LED_SLEEP", 0x06: "LED_SUSPEND", 0x07: "LED_MUTE", 0x08: "LED_MISC", }), "EV_REP": EnumDict({ 0x00: "REP_DELAY", 0x01: "REP_PERIOD", }), "EV_SND": EnumDict({ 0x00: "SND_CLICK", 0x01: "SND_BELL", }), } def get_wiimote_uinput_user_dev(): STRPK_UINPUT_USER_DEV = "80sHHHHi" + 64*4*'I' WIIMOTE_UUD_STR = [ "Nintendo Wiimote", # Device name BUS_USB, # Bus type 1, # Vendor 1, # Product 1, # Version 0 # ff_effects_max ] for f in range(64*1): #absmin WIIMOTE_UUD_STR.append(0x00) for f in range(64*1): #absmax WIIMOTE_UUD_STR.append(0x400) for f in range(64*2): #absfuzz,absflat WIIMOTE_UUD_STR.append(0x00) WIIMOTE_UUD = struct.pack( STRPK_UINPUT_USER_DEV, *WIIMOTE_UUD_STR ) return WIIMOTE_UUD def get_wiimote_evbits(): UINPUT_IO_CHANNELS = [] for ioc in IO_CHANNELS: if IO_CHANNELS[ioc] == 'UINPUT': UINPUT_IO_CHANNELS.append(ioc) UINPUT_UUD_BITS = { 'EV_REL': [ [ UI_SET_EVBIT, EV_REL ], [ UI_SET_RELBIT, REL_X ], [ UI_SET_RELBIT, REL_Y ] ], 'EV_ABS': [ [ UI_SET_EVBIT, EV_ABS ], [ UI_SET_ABSBIT, ABS_X ], [ UI_SET_ABSBIT, ABS_Y ] ], 'EV_KEY': [ [ UI_SET_EVBIT, EV_KEY ], [ UI_SET_EVBIT, EV_SYN ] ] } if IO_CHANNELS['EV_KEY'] == "UINPUT": for btn in commandMap: com = commandMap[btn] type = com[0] if type == "click" or type == "key": key = com[1] code = int(codeMaps["EV_KEY"].toNumber(key)) if code: log( LOG_INFO, "Registering key %s for button %s with code %x" % (key, btn, code) ) UINPUT_UUD_BITS['EV_KEY'].append( [UI_SET_KEYBIT, code] ) UINPUT_UUD = [] for ioc in UINPUT_IO_CHANNELS: for bitset in UINPUT_UUD_BITS[ioc]: UINPUT_UUD.append(bitset) print UINPUT_UUD return UINPUT_UUD def send_led_command(): command_start = 0x521100 command_end = 0x00 if led_state[0]: command_end += 0x10 if led_state[1]: command_end += 0x20 if led_state[2]: command_end += 0x40 if led_state[3]: command_end += 0x80 command = hex(command_start + command_end) cmd = command[2:].decode("hex") send_command( cmd ) def led_on(led): led_state[led] = 1 send_led_command() def led_off(led): led_state[led] = 0 send_led_command() def led_toggle(led): if led_state[led] == 0: led_state[led] = 1 elif led_state[led] == 1: led_state[led] = 0 send_led_command() def log(level, msg): if level & LOG_LEVEL: print msg def find_willing_wiimote(): log(LOG_INFO, "Now trying to discover a willing Wiimote, please activate your Wiimote within 5 seconds.") bt_devs = discover_devices(lookup_names = True) if bt_devs: log(LOG_INFO, "Found %d Bluetooth Devices!" % len(bt_devs) ) for bt_dev in bt_devs: if bt_dev[1] == WIIMOTE_NAME: addr = bt_dev[0] log(LOG_INFO, "Found a Wiimote at address " + addr) return addr else: log(LOG_ERR, "FAILURE!") def find_wiimote_services(addr): log(LOG_INFO, "Looking for Wiimote services at address " + addr) servs = find_service( address = addr ) if servs: log(LOG_INFO, "Victory! We have found that Wiimote!") return servs if not servs: log(LOG_ERR, "Failure. We have not found that Wiimote.") return 0 def toHex(s): l = [] for c in s: h = hex(ord(c)).replace('0x', '') if len(h) == 1: h = '0' + h l.append(h) return string.join(l, " ") def hex2s(h): return ('%x' % h).decode("hex") def send_command_code(cc): valid_names = [ 'led', 'leds', 'quit', 'key', 'click', 'vibrate', 'guess' ] name = cc[0] if name == "led" and cc[1] == "toggle": led_id = cc[2]-1 led_toggle( led_id ) elif name == 'leds' and cc[1] == "off": cs = hex2s(commandcodes['leds_off']) send_command( cs ) elif name == 'quit': osd_show( "**HOME Menu** [Back] GuessOrientation ExitWMD" ) status['go']= 1 elif name == 'guess' and cc[1] == 'orien': guess_orientation(force_log) elif name == 'click' or name == "key": keyname = cc[1] presstype = cc[2] keycode = codeMaps["EV_KEY"].toNumber( keyname ) if IO_MODES['PYOSD']: osd_show( "%s on\n %s\n (%s)" % (name, keyname, presstype) ) if IO_MODES['UINPUT'] and IO_CHANNELS['EV_KEY'] == 'UINPUT': if presstype == "DOWN": uinput_send_keydown( keycode ) elif presstype == "UP": uinput_send_keyup( keycode ) elif name == 'vibrate': if cc[1] == "toggle" and cc[2] == "DOWN": Vibe.step() elif cc[1] == "off": Vibe.off() IR.on() else: log(LOG_ERR, "COMMAND ERROR FOR COMMAND NAME = " + name) class Vibrator: """OH YEAH""" def __init__( self ): self.state = "off" self.stepn = 0 def on( self ): self.cmd_on() self.state = "on" self.stepn = 0 def off( self ): self.cmd_off() self.state = "off" self.stepn = 0 def step( self, duration=2 ): self.stepn += 1.0 self.stepn *= 1.1 freq = self.stepn n_steps = int(duration * freq) step_duration = 1/freq if self.state == "on": self.off() for i in range(n_steps): time.sleep(step_duration) self.cmd_on() time.sleep(step_duration) self.cmd_off() def cmd_on( self ): send_command(hex2s(commandcodes['vibrate_on'])) def cmd_off( self ): send_command(hex2s(commandcodes['vibrate_off'])) def send_command(commandcode): fs = '' for b in commandcode: fs += str(b).encode("hex").upper() + " " log(DEBUG_BT_SEND, "sending " + str(len (commandcode)) + " bytes: " + fs) socket['control'].send( commandcode ) time.sleep(0.01) def preprocess_force( force ): for ax in force: f_raw = force[ax].decode("hex") f_val = int(ord(f_raw)) force[ax] = f_val return force def process_force( force ): force = preprocess_force( force ) if len(force_log) > force_log_maxlen: force_log.pop(0) force_log.append(force) disturbances = {} last_axv = {} i = 0 axes = ['x','y','z'] for f in force_log: for ax in axes: v = f[ax] if i == 0: last_axv[ax] = v disturbances[ax] = 0 else: disturbances[ax] += abs(last_axv[ax]-v) last_axv[ax] = v i = i + 1 disturbed_axes = [] for ax in axes: v = disturbances[ax] if v > force_disturbance: disturbed_axes.append(ax) if quash['age'] > 0: quash['age'] -= 1 elif disturbed_axes: log(LOG_FORCE, "I SENSE A GREAT DISTURBANCE IN THE FORCE..." + str(disturbances)) quash['age'] = quash['duration'] log(DEBUG_FORCE, "Force log: " + str(force_log)) def guess_orientation_iter( orien, lowX, lowZ, accX, accZ ): lowX = (lowX * 0.9) + (accX * 0.1) lowZ = (lowZ * 0.9) + (accZ * 0.1) absX = abs(lowX - 128) absZ = abs(lowZ - 128) if orien == 0 or orien == 2: absX -= HYST elif orien == 1 or orien == 3: absZ -= HYST if absZ >= absX: if absZ > HYST: if lowZ > 128: orien = 0 else: orien = 2 elif absX > absZ: if absX > HYST: if lowX > 128: orien = 3 else: orien = 1 return [orien, lowX, lowZ] def guess_orientation(force_log): orien = 0 lowX = 128 lowZ = 160 for f in force_log: (orien, lowX, lowZ) = guess_orientation_iter( orien, lowX, lowZ, f['x'], f['z'] ) print "orien=%d, lowX=%d, lowZ=%d " % (orien, lowX, lowZ) return orien class bf(object): def __init__(self,value=0): self._d = value def __getitem__(self, index): return (self._d >> index) & 1 def __setitem__(self,index,value): value = (value&1L)<> start) & mask def __setslice__(self, start, end, value): mask = 2L**(end - start) -1 value = (value & mask) << start mask = mask << start self._d = (self._d & ~mask) | value return (self._d >> start) & mask def __int__(self): return self._d class IR_Pointer: def __init__( self ): pass def preprocess_dots( self, dots ): dots_sum = 0 dots_on = 0 for dn in range(2): dot_sum = 0 for dax in range(3): dotc_raw = dots[dn][dax].decode("hex") dotc_val = int(ord(dotc_raw)) dots[dn][dax] = dotc_val dots_sum += dotc_val dot_sum += dotc_val lx = dots[dn][0] ly = dots[dn][1] ib = bf( dots[dn][2] ) s = ib[0:3] v = ib[4] mx = bf( ib[4:6] ) my = bf( ib[6:8] ) x = bf(lx) y = bf(ly) x[8] = mx[0] x[9] = mx[1] y[8] = my[0] y[9] = my[1] if dot_sum < 255 * 3: dots_on += 1 dots[dn][0] = int(x) dots[dn][1] = int(y) log(DEBUG_IR, "dn = %u, lx = %u, ly = %u, mx = %u, my = %u, s = %u, x = %u, y = %u" % (dn, lx, ly, mx, my, s, x, y)) if dots_sum < 255*6 and dots_on == 2: x1 = dots[0][0] y1 = dots[0][1] x2 = dots[1][0] y2 = dots[1][1] dx = x1 - x2 dy = y1 - y2 d = math.sqrt( dx*dx + dy*dy ) print d return dots else: return 0 def process_dots( self, dots ): dots = self.preprocess_dots(dots) if len(dots_fifo)+1 > dots_fifo_len: dots_fifo.pop(0) if dots: dots_fifo.append(dots) else: return 0 dots_fifo_sum = [[0,0],[0,0]] dots_fifo_avg = [[0,0],[0,0]] dots_sum_dn = [0,0] dots_sum = 0 if len(dots_fifo) == dots_fifo_len: if dots_quash['age'] == 0: for dp in dots_fifo: for dn in range(2): dn_sum = 0 for dax in range(2): dotc_val = dp[dn][dax] dots_fifo_sum[dn][dax] += dotc_val dots_sum += dotc_val dn_sum += dotc_val dots_sum_dn[dn] += dn_sum for dn in range(2): for dax in range(2): dots_fifo_avg[dn][dax] = dots_fifo_sum[dn][dax] / dots_fifo_len x1 = dots_fifo_avg[0][0] y1 = dots_fifo_avg[0][1] x2 = dots_fifo_avg[1][0] y2 = dots_fifo_avg[1][1] log(DEBUG_IR, "Got two lit dots: %u,%u : %u,%u" % (x1, y1, x2, y2)) self.process_abs_report(x1, x2, y1, y2) dots_quash['age'] = dots_quash['duration'] else: dots_quash['age'] -= 1 def process_abs_report( self, x1, x2, y1, y2 ): if IO_MODES['XLIB'] and IO_CHANNELS['EV_ABS'] == 'XLIB': abs_xlib = self.abs_report_processor(x1, x2, y1, y2, XL['w'], XL['h'], 1, 0) xlib_abs_report( *abs_xlib ) # This mode is poorly tested if IO_MODES['UINPUT'] and IO_CHANNELS['EV_ABS'] == 'UINPUT': xscale = 1024 yscale = 1024 abs_uinput = self.abs_report_processor(x1, x2, y1, y2, xscale, yscale, 0, 1) uinput_abs_report( *abs_uinput ) def abs_report_processor( self, x1, x2, y1, y2, xscale, yscale, xinv, yinv ): xrange = 1024 yrange = 768 sx = self.abs_scale_axis( x1, x2, xscale, xinv, xrange, XDZ ) sy = self.abs_scale_axis( y1, y2, yscale, yinv, yrange, YDZ ) (sx, sy) = self.abs_smoothen( sx, sy ) log(LOG_IR, "averaged and scaled to " + str(sx) + ", " + str(sy) ) return [sx, sy] screenx = 0 screeny = 0 def abs_smoothen( self, x, y ): dx = ( self.screenx - x ) dy = ( self.screeny - y ) d = math.sqrt( dx*dx + dy*dy ) threshes = [ [ 10, 0.90 ], [ 20, 0.82 ], [ 35, 0.76 ], [ 50, 0.70 ], [ 70, 0.30 ], [ 90, 0.10 ], ] SS1 = 180 SS2 = 200 SS3 = 0.001 SAW = 0.5 sx = x sy = y for thresh in threshes: dMin = thresh[0] if d < dMin: Wei1 = thresh[1] Wei2 = SS1 / ( SS2 + d*d*d* SS3 ) Wei = ( Wei1 * SAW ) + (Wei2 * ( 1-SAW ) ) sx = ( self.screenx * Wei ) + ( x * (1-Wei) ) sy = ( self.screeny * Wei ) + ( y * (1-Wei) ) print "%s %s %s" % (Wei1, Wei2, Wei) break self.screenx = sx self.screeny = sy if (1): odx = ( sx - x ) ody = ( sx - y ) od = math.sqrt( odx*odx + ody*ody ) log(LOG_IR, "d=%d, x=%d, y=%d, od=%d sx=%d, sy=%d" % (d, x, y, od, sx, sy) ) return [sx, sy] def abs_scale_axis( self, p1, p2, scale, inv, range, dz ): avgp = ( p1 + p2 ) / 2 sp = avgp * ( ( scale / (1-dz) ) / range ) sp -= (scale * dz) sp = int(sp) if sp < 0: sp = 0 elif sp >= scale: sp = scale-1 if inv: sp = scale - sp return sp class Wiimote_Report_Parser: """Receives report packets from the Wiimote Classifies and then slices them Individually processes and dispatches the slices """ SLICE_BTN = [ 4, 8, "slice_BTN" ] SLICE_ACC = [ 8, 14, "slice_ACC" ] SLICE_IR = [ 14, 26, "slice_IR" ] SLICE_IRBTN = [ 0, 0, "slice_IRBTN" ] def __init__( self ): self.REPORT_TYPES = { 4: [ "BTN_ONLY", # Button status only [ self.SLICE_BTN ] ], 6: [ "IR_BTN", # Useless? Button status reports that are sent during IR mode - allows for lower button latency!! [ self.SLICE_IRBTN ] ], 7: [ "BTN_ACC", # Buttons+accelerometers status [ self.SLICE_BTN, self.SLICE_ACC ] ], 19:[ "BTN_ACC_IR", # Buttons+accelerometers+IR sensor status [ self.SLICE_BTN, self.SLICE_ACC, self.SLICE_IR ] ] } def parse( self, report ): log(DEBUG_BT_RECV, "Recv len(" + str(len(report)) + "): " + str(string.split(toHex(report), " "))) if self.REPORT_TYPES.has_key( len(report) ): type = self.REPORT_TYPES[ len(report) ] self.split_report( report, type ) else: log(LOG_ERR, "Invalid packet size: " + str(len(report))) if not LOG_LEVEL & DEBUG_BT_RECV: log(LOG_ERR, "Recv len(" + str(len(report)) + "): " + str(string.split(toHex(report), " "))) def split_report( self, report, type ): hex_report = report.encode("hex") slicedefs = type[1] for slicedef in slicedefs: sta = int(slicedef[0]) end = int(slicedef[1]) funcname = slicedef[2] slice = hex_report[ sta: end ] func = getattr(self,funcname) func( slice ) def slice_BTN( self, slice ): rawbtd = int( slice, 16 ) which_buttons( rawbtd ) def slice_IRBTN( self, slice ): pass def slice_ACC( self, slice ): force = { 'x': slice[0:2], 'y': slice[2:4], 'z': slice[4:6] } process_force(force) def slice_IR( self, slice ): dots = [ [ slice[0:2], slice[2:4], slice[4:6] ], [ slice[6:8], slice[8:10], slice[10:12] ] ] IRPointer.process_dots(dots) def update_button_states(btps): bts_up = [] bts_down = [] for bt in buttonstates: oldbuttonstates[bt] = buttonstates[bt] buttonstates[bt] = 0 for bt in btps: buttonstates[bt] = 1 for bt in oldbuttonstates: if oldbuttonstates[bt] and not buttonstates[bt]: bt_up(bt) bts_up.append(bt) if not oldbuttonstates[bt] and buttonstates[bt]: bt_down(bt) bts_down.append(bt) if bts_down: log(LOG_BTN, "Buttons down: " + str(bts_down)) if bts_up: log(LOG_BTN, "Buttons up: " + str(bts_up)) def which_buttons(rawbtd): btps = [] if rawbtd != 0: for bt in buttonmap: btk = buttonmap[bt] if btk & rawbtd: btps.append(bt) update_button_states( btps ) def bt_down( bt ): command = copy( commandMap[bt] ) command.append( "DOWN" ) send_command_code( command ) time.sleep(0.1) def bt_up( bt ): command = copy( commandMap[bt] ) command.append( "UP" ) send_command_code( command ) time.sleep(0.1) def send(cmd,report,data): c = chr(cmd) + chr(report) for d in data: c += chr(d) send_command( c ) def setmode(mode,cont): aux = 0 rmbl = 0 if rmbl: aux |= 0x01 if cont: aux |= 0x04 send(CMD_SET_REPORT,RID_MODE,[aux,mode]) # size here is redundant, since we can just use len(data) if we want. def senddata(data,offset,size): # see writing to data: [[#On-board Memory]. of1 = offset >> 24 & 0xFF #extract offset bytes of2 = offset >> 16 & 0xFF of3 = offset >> 8 & 0xFF of4 = offset & 0xFF data2 = data + [0]*(16-len(data)) # append zeros to pad data if less than 16 bytes if len(data2) > 16: data2 = data2[:16] # crop data if we have too much # format is [OFFSET (BIGENDIAN),SIZE,DATA (16bytes)] send(CMD_SET_REPORT,RID_WMEM,[of1,of2,of3,of4,size]+data2) def bt_connect(addr): socket['receive'] = BluetoothSocket( L2CAP ) socket['control'] = BluetoothSocket( L2CAP ) socket['receive'].connect( ( addr, 19 ) ) socket['control'].connect( ( addr, 17 ) ) if socket['receive'] and socket['control']: log(LOG_INFO, "We are now connected to Wiimote at address " + addr) return 1 def bt_disconnect(): socket['receive'].close() socket['control'].close() log(LOG_INFO, "Disconnected") class Mode_IR: def __init__( self, sequence="Ian", state="off" ): self.sequences = { 'Ian': self.seq_ian, 'Cliff': self.seq_cliff, 'Marcan': self.seq_marcan } if self.sequences.has_key( sequence ): self.sequence = self.sequences[sequence] self.state = "off" if state == "on": self.state = "on" self.sequence() def on( self ): self.state = "on" self.sequence() def off( self ): self.state = "off" def seq_marcan( self ): # this seems to be the minimal code to get it to work setmode(MODE_ACC_IR,0) send(CMD_SET_REPORT,RID_IR_EN,[FEATURE_ENABLE]) send(CMD_SET_REPORT,RID_IR_EN2,[FEATURE_ENABLE]) senddata([8],0x04B00030,1) # enable IR data out senddata([0x90],0x04B00006,1) # sensitivity constants (guessed, Cliff seems to have more data, but this works for me) senddata([0xC0],0x04B00008,1) senddata([0x40],0x04B0001A,1) senddata([IR_MODE_EXP],0x04B00033,1) # enable IR output with specified data format def seq_cliff( self ): # this is Cliff's version pythonified, probably more accurate as far as sensitivity. Works pretty much the same for me. setmode(MODE_ACC_IR,0) send(CMD_SET_REPORT,RID_IR_EN,[FEATURE_ENABLE]) send(CMD_SET_REPORT,RID_IR_EN2,[FEATURE_ENABLE]) senddata([1],0x04B00030,1) # seems to enable the IR peripheral senddata([0x02, 0x00, 0x00, 0x71, 0x01, 0x00, 0xaa, 0x00, 0x64],0x04B00000,9) senddata([0x63, 0x03],0x04B0001A,2) # this seems incorrect - for FULL IR mode, we must use FULL wiimote mode (0x3e). # otherwise the data is probably garbled. senddata([IR_MODE_FULL],0x04B00033,1) senddata([8],0x04B00030,1) # Enable data output. Can be specified first it seems, we don't really need to be in mode 1. def seq_ian( self ): for i in range(3): setmode(MODE_ACC_IR,0) time.sleep(0.005) send(CMD_SET_REPORT,RID_IR_EN,[FEATURE_ENABLE]) time.sleep(0.003) send(CMD_SET_REPORT,RID_IR_EN2,[FEATURE_ENABLE]) dataset = [ [ 0x04B00030, 0x01 ], [ 0x04B00030, 0x08 ], [ 0x04B00006, 0x90 ], [ 0x04B00008, 0xC0 ], [ 0x04B0001A, 0x40 ], [ 0x04B00033, 0x33 ], [ 0x04B00030, 0x08 ] ] for d in dataset: time.sleep(0.001) senddata( [ d[1] ], d[0], 1 ) time.sleep(0.05) def start_uinput(): uinput = os.open(UINPUT_DEV, os.O_RDWR) os.write(uinput, WIIMOTE_UUD); for i in WIIMOTE_EVBITS: bit = i[0] val = i[1] fcntl.ioctl(uinput, bit, val) fcntl.ioctl(uinput, UI_DEV_CREATE) return uinput def ui_send_event(evtype, evcode, evval): if IO_MODES['UINPUT'] and socket['uinput']: STRPK_INPUT_EVENT = "LLHHi" evstruct = struct.pack(STRPK_INPUT_EVENT, time.time(), 0, evtype, evcode, evval) os.write( socket['uinput'], evstruct ) def test_uinput(reps): theta = 0 for a in range(reps): x_rel = int(math.sin(theta) * 10) y_rel = int(math.cos(theta) * 10) theta += 0.15 ui_send_event( EV_REL, REL_X, x_rel ) ui_send_event( EV_REL, REL_Y, y_rel ) ui_send_event( EV_SYN, SYN_REPORT, 0 ) time.sleep(0.01) def uinput_sendkey(KEY_CONST): log(LOG_BTN, "Send Key: %d" % KEY_CONST) uinput_send_keydown(KEY_CONST) uinput_send_keyup(KEY_CONST) def uinput_send_keydown(KEY_CONST): log(LOG_BTN, "Send Key DOWN: %d" % KEY_CONST) ui_send_event( EV_KEY, KEY_CONST, 1 ) ui_send_event( EV_SYN, SYN_REPORT, 0 ) def uinput_send_keyup(KEY_CONST): log(LOG_BTN, "Send Key UP: %d" % KEY_CONST) ui_send_event( EV_KEY, KEY_CONST, 0 ) ui_send_event( EV_SYN, SYN_REPORT, 0 ) def test_uinput3(): x_abs = random.randint(0,255) y_abs = random.randint(0,255) uinput_abs_report( x_abs, y_abs ) def uinput_abs_report( x_abs, y_abs ): ui_send_event( EV_ABS, ABS_X, x_abs ) ui_send_event( EV_ABS, ABS_Y, y_abs ) ui_send_event( EV_SYN, SYN_REPORT, 0 ) time.sleep(0.01) OSD = {} def start_pyosd(): import pyosd font = "-*-helvetica-medium-r-normal-*-*-250-*-*-p-*-*-*" OSD['p'] = pyosd.osd( font=font, offset=200, colour="#2222cc" ) OSD['p'].set_align( pyosd.ALIGN_CENTER ) def osd_show(msg): OSD['p'].display(msg) XL = {} def start_xlib(): from Xlib import display ENV_DISPLAY = os.environ.get("DISPLAY") d = display.Display( ENV_DISPLAY ) i = d.screen() XL['d'] = d XL['w'] = SCREEN_WIDTH or int( i['width_in_pixels'] ) XL['h'] = SCREEN_HEIGHT or int( i['height_in_pixels'] ) def xlib_abs_report( x_abs, y_abs ): d = XL['d'] d.screen().root.warp_pointer( x_abs, y_abs ) d.sync() ReportParser = Wiimote_Report_Parser() def main_loop(): while status['go']: data = socket['receive'].recv(1024) if len(data): ReportParser.parse( data ) def get_wiimote_addr(): servs = 0 if len(MY_WIIMOTE_ADDR): addr = MY_WIIMOTE_ADDR servs = find_wiimote_services( addr ) if servs: return addr else: addr = find_willing_wiimote() servs = find_wiimote_services( addr ) if servs: return addr else: log(DEBUG_INFO, "No luck finding Wiimote services.") return 0 WIIMOTE_UUD = get_wiimote_uinput_user_dev() WIIMOTE_EVBITS = get_wiimote_evbits() Vibe = Vibrator() IR = Mode_IR() IRPointer = IR_Pointer() addr = get_wiimote_addr() if addr: bt_connect(addr) send_command_code( ['leds', 'off'] ) if IR_ENABLE: IR.on() if IO_MODES['UINPUT']: socket['uinput'] = start_uinput() if IO_MODES['XLIB']: start_xlib() if IO_MODES['PYOSD']: start_pyosd() main_loop() bt_disconnect()