#!/usr/bin/env python3 # File: pyjstest.py # Location: https://bgstack15.ddns.net/git/pyjstest/ # Author: bgstack15 # SPDX-License-Identifier: GPL-3.0 # Startdate: 2022-06-29 # Title: Pyjstest # Package: pyjstest # Purpose: Display inputs of one connected gamepad # History: # Usage: # Adjust config.py and then run! # Reference: # https://stackoverflow.com/a/50572903 # https://pysdl2.readthedocs.io/en/0.9.11/tutorial/pong.html # Improve: # display text of axes values? https://github.com/caerus706/Python3-pySDL2-examples/blob/master/text.py # Documentation: See README.md import ctypes, time, random from sdl2 import * import sdl2.ext, sys from config import colors, CONFIG class SoftwareRenderer(sdl2.ext.SoftwareSpriteRenderSystem): def __init__(self, window): super(SoftwareRenderer, self).__init__(window) def render(self, components): sdl2.ext.fill(self.surface, colors["BKGD"]) super(SoftwareRenderer, self).render(components) class MovementSystem(sdl2.ext.Applicator): def __init__(self, minx, miny, maxx, maxy): super(MovementSystem, self).__init__() self.componenttypes = Velocity, sdl2.ext.Sprite self.minx = minx self.miny = miny self.maxx = maxx self.maxy = maxy def process(self, world, componentsets): for velocity, sprite in componentsets: swidth, sheight = sprite.size sprite.x += velocity.vx sprite.y += velocity.vy sprite.x = max(self.minx, sprite.x) sprite.y = max(self.miny, sprite.y) pmaxx = sprite.x + swidth pmaxy = sprite.y + sheight if pmaxx > self.maxx: sprite.x = self.maxx - swidth if pmaxy > self.maxy: sprite.y = self.maxy - sheight class Velocity(object): def __init__(self): super(Velocity, self).__init__() self.vx = 0 self.vy = 0 class Rectangle(sdl2.ext.Entity): def __init__(self, world, sprite, posx=0, posy=0): self.world = world self.sprite = sprite self.sprite.position = posx, posy self.velocity = Velocity() class TwoAxisBackground(sdl2.ext.Entity): def __init__(self, world, factory, color, posx=0, posy=0, sizex=100, sizey=100): self.world = world self.sprite = factory.from_color(color, size=(sizex, sizey)) self.sprite.position = posx, posy class Button(object): def __init__(self, world, factory, color_on, color_off, posx, posy, size, button, relx=0, rely=0, owner = None): self.world = world self.factory = factory self.color_on = color_on self.color_off = color_off self.posx = posx self.posy = posy self.size = size self.object = Rectangle(self.world, self.factory.from_color(self.color_off, size=(self.size, self.size)), self.posx, self.posy) self.status = False self.button = button self.type = "button" self.relx = relx self.rely = rely self.owner = owner # either None, or an Analog def on(self): if not self.status: self.object.sprite = self.factory.from_color(self.color_on, size=(self.size,self.size)) if self.owner is None: self.object.sprite.position = self.posx+self.relx, self.posy+self.rely self.status = True def off(self): if self.status: self.object.sprite = self.factory.from_color(self.color_off, size=(self.size,self.size)) if self.owner is None: self.object.sprite.position = self.posx+self.relx, self.posy+self.rely self.status = False class Dpad(object): """ Represent simple UDLR position of an analog stick. One Dpad object per direction per axis excluding off. """ def __init__(self, world, factory, color_on, color_off, posx, posy, size, axis, direction): self.world = world self.factory = factory self.color_on = color_on self.color_off = color_off self.posx = posx self.posy = posy self.size = size self.type = "dpad" self.axis = axis self.direction = direction self.object = Rectangle(self.world, self.factory.from_color(self.color_off, size=(self.size,self.size)), self.posx, self.posy) self.status = False def positive(self, axis): #print(f"Dpad axis {axis} moved positive.") self.on() if "positive" == self.direction else self.off() def negative(self, axis): #print(f"Dpad axis {axis} moved negative.") self.on() if "negative" == self.direction else self.off() def middle(self, axis): self.off() def on(self): if not self.status: #self.object = Rectangle(self.world, self.factory.from_color(self.color_on, size=(self.size,self.size)), self.posx, self.posy) self.object.sprite = self.factory.from_color(self.color_on, size=(self.size,self.size)) self.object.sprite.position = self.posx, self.posy self.status = True def off(self): if self.status: #self.object = Rectangle(self.world, self.factory.from_color(self.color_off, size=(self.size,self.size)), self.posx, self.posy) self.object.sprite = self.factory.from_color(self.color_off, size=(self.size,self.size)) self.object.sprite.position = self.posx, self.posy self.status = False def react(self, config, axis, value): #print(f"Dpad axis {axis} value {value}") if value >= config["dpad_axes"]["min"][axis] and value < config["dpad_axes"]["mid_min"][axis]: self.negative(axis) elif value >= config["dpad_axes"]["mid_min"][axis] and value <= config["dpad_axes"]["mid_max"][axis]: self.middle(axis) elif value > config["dpad_axes"]["mid_max"][axis] and value <= config["dpad_axes"]["max"][axis]: self.positive(axis) else: print(f"Warning! For Dpad {self} axis {axis}, value {value} is undefined behavior.") class Analog(object): def __init__(self, world, factory, color_on, color_off, centerx, centery, indicator_size, size, axes, orientations, buttons): self.world = world self.factory = factory self.color_on = color_on self.color_off = color_off self.posx = centerx self.posy = centery self.size = size self.type = "analog" self.axes = axes self.indicator_size = indicator_size self.orientations = orientations self.buttons_in = buttons # a list of ["color_on", "color_off", button_id] self.ulx = int(self.posx-(size/2)) self.uly = int(self.posy-(size/2)) self.lrx = int(self.posx+(size/2)) self.lry = int(self.posy+(size/2)) self.background = TwoAxisBackground(self.world, self.factory, self.color_off, self.ulx, self.uly, size, size) self.indicator = Rectangle(self.world, self.factory.from_color(self.color_on, size=(self.indicator_size,self.indicator_size)), self.posx, self.posy) #self.movement = MovementSystem(self.ulx, self.uly, self.lrx, self.lry) #self.world.add_system(self.movement) self.buttons = [] for b in buttons: self.buttons.append(Button(self.world,self.factory,colors[b[0]],colors[b[1]],self.posx,self.posy,b[4],b[5],b[2],b[3],self)) def react(self, config, axis, value): #print(f"Updating with {axis}, {value}") _max = config["dpad_axes"]["max"][axis] _min = config["dpad_axes"]["min"][axis] percent = (value - _min) / (_max - _min) * 100 current_position = self.indicator.sprite.position # (x, y) newx = current_position[0] newy = current_position[1] # X is axes[0] if axis == self.axes[0]: newx = int((self.ulx+(self.size*percent/100))-(self.indicator_size/2)) elif axis == self.axes[1]: newy = int((self.uly+(self.size*percent/100))-(self.indicator_size/2)) self.indicator.sprite.position = newx, newy for b in self.buttons: b.object.sprite.position = (newx+b.relx), (newy+b.rely) return newx, newy class Hat(object): def __init__(self, world, factory, color_on, color_off, posx, posy, size, hat, off, up, right, down, left): self.world = world self.factory = factory self.color_on = color_on self.color_off = color_off self.hat = hat self.basex = posx self.basey = posy self.size = size self.type = "hat" # center, up, right, down, left self.posx = [0, 0, 20, 0, -20] self.posy = [0, -20, 0, 20, 0] self.box = [] for i in range(0,5): self.box.append(Rectangle(self.world, self.factory.from_color(self.color_off, size=(self.size,self.size)), self.basex+self.posx[i], self.basey+self.posy[i])) # array of binary values that will describe the current hat position self.binary = [off, up, right, down, left] def react(self, value): print(f"Hat {self.hat} value {value}") possible = [0,1,2,3,4] on = [] if value == self.binary[0]: on.append(0) for r in range(1,5): # 1 to 4 if value & self.binary[r]: on.append(r) # evaluate all the options off = possible for option in on: off.remove(option) #print(f"Hat options on: {on}") #print(f"Hat options off: {off}") x = 0 while x < 5: self.box = [] if x in on: self.box.append(Rectangle(self.world, self.factory.from_color(self.color_on, size=(self.size,self.size)), self.basex+self.posx[x], self.basey+self.posy[x])) else: self.box.append(Rectangle(self.world, self.factory.from_color(self.color_off, size=(self.size,self.size)), self.basex+self.posx[x], self.basey+self.posy[x])) x = x + 1 # color_on, color_off, x,y, size, axis, direction #["GRAY", "DARK_GRAY", 60, 40, 20, 2, 1], class Trigger(object): """ Direction is not actually implemented. """ def __init__(self, world, factory, renderer, color_on, color_off, color_bkgd, x1, y1, x2, y2, size, axis, direction): self.world = world self.factory = factory self.color_on = color_on self.color_off = color_off self.color_bkgd = color_bkgd self.axis = axis self.x1 = x1 self.y1 = y1 self.x2 = x2 self.y2 = y2 self.boxx1 = min(x1,x2) self.boxy1 = min(y1,y2) self.boxx2 = max(x1,x2) self.boxy2 = max(y1,y2) self.linex1 = x1 - self.boxx1 self.liney1 = y1 - self.boxy1 self.linex2 = x2 - self.boxx1 self.liney2 = y2 - self.boxy1 self.type = "trigger" self.indicator_size = size # This line makes rectangle the minimum size so the drawn line actually displays. self.rectangle_size = max((self.boxx2-self.boxx1),1),max((self.boxy2-self.boxy1),1) print(f"Drawing rectangle, {self.color_bkgd}, size ({self.rectangle_size}), starting ({self.boxx1},{self.boxy1})") self.background = Rectangle(self.world, self.factory.from_color(self.color_bkgd, size=self.rectangle_size), self.boxx1, self.boxy1) print(f"Drawing line color {self.color_off}, from ({self.linex1}, {self.liney1}) to ({self.linex2},{self.liney2})") sdl2.ext.draw.line(self.background.sprite, self.color_off, (self.linex1, self.liney1, self.linex2, self.liney2)) self.indicator = Rectangle(self.world, self.factory.from_color(self.color_on, size=(self.indicator_size,self.indicator_size)), self.x1, self.y1) def react(self, config, axis, value): """ This one-axis trigger will move in X,Y dimensions based on the line drawn. """ print(f"Axis {axis} value {value}") _max = config["dpad_axes"]["max"][axis] _min = config["dpad_axes"]["min"][axis] percent = (value - _min) / (_max - _min) * 100 current_position = self.indicator.sprite.position # (x, y) newx = int((self.x2 - self.x1)*percent/100-(self.indicator_size/2) + self.x1) +1 newy = int((self.y2 - self.y1)*percent/100-(self.indicator_size/2) + self.y1) +1 self.indicator.sprite.position = newx, newy def find_joystick_config_by_alias(config, name): """ Given CONFIG and a joystick name, see if that joystick name shows up as either a ruleset or alias for a ruleset, and then return the proper ruleset name. """ if name in config: print(f"DEBUG (fjcba): Found name {name} in config.") return name else: for c in config: if "aliases" in config[c]: if name in config[c]["aliases"]: print(f"DEBUG (fjcba): Found name {c} that matches alias {name}.") return c print(f"DEBUG (fjcba): oops! Found nothing that matches {name}.") return None def run(): world = sdl2.ext.World() gamepads = [] #joystick = Joystick() SDL_Init(SDL_INIT_JOYSTICK) sdl2.ext.init() joystick = "" # load resources/ directory #RESOURCES = sdl2.ext.Resources(__file__,"resources") factory = sdl2.ext.SpriteFactory(sdl2.ext.SOFTWARE) #sprite = factory.from_image(RESOURCES.get_path("gamepad.png")) input_array = [] #if "buttons" in CONFIG[joystick]: # for b in CONFIG[joystick]["buttons"]: # input_array.append(button(world,factory,colors[b[0]],colors[b[1]],b[2],b[3],b[4])) #if "dpads" in CONFIG[joystick]: # for b in CONFIG[joystick]["dpads"]: # input_array.append(Dpad(world,factory,colors[b[0]],colors[b[1]],b[2],b[3],b[4],b[5])) #if "hats" in CONFIG[joystick]: # for b in CONFIG[joystick]["hats"]: # input_array.append(Hat(world, factory, colors[b[0]], colors[b[1]], b[2], b[3], b[4], b[5],b[6],b[7],b[8],b[9])) #window = sdl2.ext.Window("Pyjoy", size=CONFIG[joystick]["window"]) #window = sdl2.ext.Window("Pyjoy", size=(100,100), flags = SDL_WINDOW_RESIZABLE) window = sdl2.ext.Window("Pyjoy", size=(100,100)) window.show() spriterenderer = SoftwareRenderer(window) world.add_system(spriterenderer) running = True status = {} while running: event = SDL_Event() while SDL_PollEvent(ctypes.byref(event)) != 0: if event.type == SDL_JOYDEVICEADDED: #self.device = SDL_JoystickOpen(event.jdevice.which) joystick = SDL_JoystickName(SDL_JoystickOpen(event.jdevice.which)).decode('UTF-8') print(f"Device: {joystick}") config_name = find_joystick_config_by_alias(CONFIG,joystick) window.title = config_name if config_name: joystick = config_name print(f"Working with controller type {config_name}") #window.size = CONFIG[joystick]["window"] SDL_SetWindowSize(window.window,CONFIG[joystick]["window"][0],CONFIG[joystick]["window"][1]) window.refresh() #window.get_surface() # Use a temporary list, to avoid the "Set size changed during iteration." temp_list = [] for i in world.entities: temp_list.append(i) for i in temp_list: i.delete() world.remove_system(spriterenderer) spriterenderer = SoftwareRenderer(window) world.add_system(spriterenderer) input_array = [] if "buttons" in CONFIG[joystick]: for b in CONFIG[joystick]["buttons"]: input_array.append(Button(world,factory,colors[b[0]],colors[b[1]],b[2],b[3],b[4],b[5])) if "dpads" in CONFIG[joystick]: for b in CONFIG[joystick]["dpads"]: input_array.append(Dpad(world,factory,colors[b[0]],colors[b[1]],b[2],b[3],b[4],b[5],b[6])) if "hats" in CONFIG[joystick]: for b in CONFIG[joystick]["hats"]: input_array.append(Hat(world, factory, colors[b[0]], colors[b[1]], b[2], b[3], b[4], b[5],b[6],b[7],b[8],b[9],b[10])) if "analogs" in CONFIG[joystick]: for b in CONFIG[joystick]["analogs"]: a = Analog(world, factory, colors[b[0]], colors[b[1]], b[2], b[3], b[4], b[5], b[6], b[7], b[8]) input_array.append(a) for b in a.buttons: input_array.append(b) if "triggers" in CONFIG[joystick]: for b in CONFIG[joystick]["triggers"]: input_array.append(Trigger(world, factory, spriterenderer, colors[b[0]], colors[b[1]], colors[b[2]], b[3], b[4], b[5], b[6], b[7], b[8], b[9])) #spriterenderer = SoftwareRenderer(window) #world.add_system(spriterenderer) #return 0 elif event.type == SDL_JOYAXISMOTION: #self.axis[event.jaxis.axis] = event.jaxis.value #print(event.jaxis.axis,event.jaxis.value) status[event.jaxis.axis] = event.jaxis.value axis = event.jaxis.axis value = event.jaxis.value for q in input_array: if "dpad" == q.type and q.axis == axis: q.react(CONFIG[joystick], axis, value) if "analog" == q.type and axis in q.axes: response = q.react(CONFIG[joystick], axis, value) #print(f"Coord of indicator: {response}") if "trigger" == q.type and axis == q.axis: q.react(CONFIG[joystick], axis, value) elif event.type == SDL_JOYHATMOTION: #print(f"hat {event.jhat.hat},{event.jhat.value}") for q in input_array: if "hat" == q.type and q.hat == event.jhat.hat: q.react(event.jhat.value) elif event.type == SDL_JOYBUTTONDOWN: #self.button[event.jbutton.button] = True print(f"{event.jbutton.button} down") b = event.jbutton.button for q in input_array: if "button" == q.type and b == q.button: q.on() elif event.type == SDL_JOYBUTTONUP: #self.button[event.jbutton.button] = False print(f"{event.jbutton.button} up") b = event.jbutton.button for q in input_array: if "button" == q.type and b == q.button: q.off() elif event.type == sdl2.SDL_QUIT: window.close() print("quitting...") running = False break else: # file /usr/lib/python3/dist-packages/sdl2/events.py # 1536 = SDL_JOYAXISMOTION but gets thrown separately too? # 1024 = SDL_MOUSEMOTION #if event.type not in [1536, 1538, 1024]: print(f"Unknown event {event.type}") result = "\r" for s in sorted(status): result += f"{s}: {status[s]:06} " print(result, end=" ") world.process() return 0 if __name__ == "__main__": running_main = True while running_main: if -1 == run(): running_main = False sys.exit(0)