#!/usr/bin/env python import json import os import sys import pygame import time class Visualizer: def __init__(self, profile): # Change to script dir to allow loading of configs relative to script if getattr(sys, 'frozen', False): # The application is frozen, figure out the path of the EXE pathname = os.path.dirname(sys.executable) else: # The application is not frozen, get PWD pathname = os.path.dirname(__file__) if pathname: os.chdir(pathname) # Load config self.config = self.load_config(profile)["profile"] # initialize the screen self.win_size = self.config["size"] pygame.init() self.screen = pygame.display.set_mode(self.config["size"], pygame.RESIZABLE) pygame.display.set_caption("SRTK Gamepad Visualizer: %s" % profile) # Load and prep the background background_file = os.path.join("profiles", profile, self.config["background_image"]) self.background = pygame.image.load(background_file).convert_alpha() # Create the buffer image to draw on, set to the size of the background image self.buffer = pygame.surface.Surface(self.background.get_size()) # Create the font self.text_font = pygame.font.SysFont(self.config["font_face"], self.config["font_size"]) self.font_color = self.config["font_color"] self.text_pos = self.config["text_pos"] # APS counter self.actions = [] self.aps_min = self.config["aps_minimum"] self.aps = 0 self.aps_throttle = 3 # Set up our inputs # Inputs are defined as key = (light position, light surface object) self.active_inputs = set() self.last_inputs = set() self.inputs = {} inputs = self.config["inputs"] for i in inputs: if "enabled" in inputs[i] and not inputs[i]["enabled"]: continue pos = inputs[i]["pos"] size = inputs[i]["size"] color = inputs[i]["color"] if "color" in inputs[i] else self.config["default_color"] img = pygame.Surface(size) img.fill(color) try: i = int(i) except: pass self.inputs[i] = (pos, img) def load_config(self, profile): infile_name = os.path.join("profiles", profile, "config.txt") infile = open(infile_name, "r") config = json.load(infile) return config def button_down(self, button_id): self.active_inputs.add(button_id) self.actions.append(time.time()) def button_up(self, button_id): try: self.active_inputs.remove(button_id) except: pass def axis_change(self, axis, value): if value > 0.1: self.active_inputs.add("A%dP" % axis) elif value < -0.1: self.active_inputs.add("A%dM" % axis) else: for a in ["A%dM" % axis, "A%dP" % axis]: try: self.active_inputs.remove(a) except: pass def hat_change(self, hat, value): # H0U, H0D, H0L, H0R if value[0] < 0: self.active_inputs.add("H%dL" % hat) elif value[0] > 0: self.active_inputs.add("H%dR" % hat) else: for a in ["H%dL" % hat, "H%dR" % hat]: try: self.active_inputs.remove(a) except: pass if value[1] < 0: self.active_inputs.add("H%dD" % hat) elif value[1] > 0: self.active_inputs.add("H%dU" % hat) else: for a in ["H%dU" % hat, "H%dD" % hat]: try: self.active_inputs.remove(a) except: pass def update(self): """ Handles screen drawing. Blits images to a buffer image, resizes the buffer to the size of the screen, then blits the buffer to the screen. """ if self.active_inputs == self.last_inputs: return None self.last_inputs = set(self.active_inputs) buf = self.buffer screen = self.screen # Blank the buffer, draw the button hilights buf.fill(self.config["background_color"]) for i in self.active_inputs: try: loc, img = self.inputs[i] buf.blit(img, loc) except KeyError: pass # Draw the background overtop as a mask buf.blit(self.background, (0, 0)) # Draw APS # Throttle keeps us from "Vibrating number syndrome" self.aps_throttle -= 1 if self.aps_throttle < 1: self.aps_throttle = 3 # Only score APS off the last 30 actions and last 5s while self.actions and self.actions[0] < (time.time() - 5): self.actions.pop(0) if len(self.actions) > 30: self.actions = self.actions[-30:] # Time from last action to current time, to get APS # We want at least 5 actions before we consider drawing APS if len(self.actions) > 5: aps_len = time.time() - self.actions[0] + 0.01 self.aps = len(self.actions) / aps_len else: self.aps = 0 # We only care about APS if we're mashing, so only draw it if it's over aps_min if self.aps > self.aps_min: aps_surf = self.text_font.render("%0.1f" % self.aps, 1, self.font_color) buf.blit(aps_surf, self.config["text_pos"]) # Scale the buffer to the screen size if necessary if buf.get_size() != screen.get_size(): buf = pygame.transform.scale(buf, screen.get_size()) # Blit buffer to screen screen.blit(buf, (0, 0)) pygame.display.flip() if __name__ == "__main__": # Read in a profile name from commandline if len(sys.argv) == 2: profile = sys.argv[1] else: profile = "universal" # Initialize all our joysticks so we get the one we want # Since this is a one-player app, this should be fine... pygame.joystick.init() joysticks = [pygame.joystick.Joystick(x) for x in range(pygame.joystick.get_count())] for joy in joysticks: joy.init() # Initialize our visualizer context vis = Visualizer(profile) quit = False while not quit: # Note that we wait for pygame events before doing any redraws # This limits redraws to when button states change, normally event = pygame.event.wait() if event.type == pygame.VIDEORESIZE: vis.win_size = (event.w, event.h) vis.screen = pygame.display.set_mode(vis.win_size, pygame.RESIZABLE) elif event.type == pygame.QUIT: quit = True elif event.type == pygame.JOYBUTTONDOWN: vis.button_down(event.button) elif event.type == pygame.JOYBUTTONUP: vis.button_up(event.button) elif event.type == pygame.JOYAXISMOTION: vis.axis_change(event.axis, event.value) elif event.type == pygame.JOYHATMOTION: vis.hat_change(event.hat, event.value) vis.update()