├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── assets ├── co.png ├── icon-12x12.png ├── icon.ico ├── mask.png └── pg.png ├── graph_viewer.py ├── keyboard_input.py ├── led_processing.py ├── led_viewer.py ├── main_window.py ├── platform_interface.py ├── profiles ├── Brittany_Stamina.rfx └── Brittany_Test.rfx ├── requirements.txt └── sensor_viewer.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | env 3 | build 4 | dist 5 | *.spec -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.linting.flake8Enabled": true, 3 | "python.linting.enabled": true 4 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Impulse Creations Ltd. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this hardware, software, and associated documentation files (the "Product"), to deal in the Product without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Product, and to permit persons to whom the Product is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Product. 6 | 7 | THE PRODUCT IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE PRODUCT OR THE USE OR OTHER DEALINGS IN THE PRODUCT. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RE:Flex Dance Python Interface 2 | 3 | ## Project Information 4 | 5 | This tool lets you use a RE:Flex Dance Pad with any PC game by acting as a keyboard that presses keys when you press panels on the dance platform. It also lets you set pressure thresholds determining how easily a panel is "pressed", and lets you display custom images on the panel LEDs when pressed. All these settings can be saved to a profile for later quick recall. 6 | 7 | It is built with Python, using HIDAPI (Human Interface Device, a USB standard; application programming interface to communicate with a single RE:Flex Dance Pad. The graphical user interface is implemented with PyQT5. 8 | 9 | The tool further acts as a reference implementation for those who wish to implement a RE:Flex interface into their own codebase. 10 | 11 | It implements the following features: 12 | - Sensor data capture / conversion - Implements an extremely rudimentary absolute value driven sensor step detection implementation. 13 | - Keyboard emulation - Maps the panels Left/Down/Up/Right to keyboard keys A/B/C/D. This is the easiest way to communicate with any / all programs. 14 | - PNG file display on panels - Converts a 12x12 PNG file and displays this on panels when it is activated. Automatically rotates each image to match the panel (default being the UP arrow). 15 | - User profiles - Can save user information to a '.rfx' file (A python dictionary stored in JSON format), making it easy to switch settings quickly. 16 | 17 | Example user profiles and LED PNG files are provided in the `profiles` and `assets` directories. 18 | 19 | ## Building the Project 20 | 21 | **These instructions were tested on Windows 10.** 22 | 23 | This project was created using [VS Code](https://code.visualstudio.com/), with the Python plugin. It uses a virtual environment for dependencies, and PyInstaller to compile the single file executable. 24 | 25 | You can install the latest [Python](https://www.python.org/downloads/) release to get started, and [add it to your PATH environment variable](https://geek-university.com/uncategorized/add-python-to-the-windows-path/). 26 | 27 | Within VS Code you can then use the provided terminal to setup your virtual environment and the dependencies. Start in your project root directory and enter: 28 | 29 | ``` 30 | python -m venv env 31 | pip install -r requirements.txt 32 | ``` 33 | 34 | You can activate your virtual environment by entering `env/Scripts/Activate.ps1` in your terminal, which is a powershell script that is automatically run by VS Code on project load. 35 | 36 | You can then run the program by entering `env/Scripts/python.exe main_window.py`. 37 | 38 | ### Building the standalone program 39 | 40 | You can build the project by running this command line script within the root directory of the project, with your virtual environment activated: 41 | 42 | ``` 43 | pyinstaller -w -F -i "assets\icon.ico" -n "reflex-config" main-window.py 44 | ``` 45 | 46 | Your program will then be in the `dist` folder. 47 | 48 | ## Project Improvements 49 | 50 | As with everything, there's a lot that we could do to make this program better. Here's a list of fixes/improvements that I'd like to see in future updates: 51 | 52 | ### Bugs / Needs Investigation 53 | 54 | - When more than one instance of the configuration tool is open, a single pad can still be accessed by both instances. This prevents both tools from communicating with the pad. 55 | - Graphing reduces the sensor / LED update frequency. This isn't the end of the world as it's toggle-able, but it warrants investigation to prevent graphing data updates from causing the data to slow down. 56 | - LED and sensor communication should run in separate threads and take the highest priority to ensure 1kHz communication to sensors and 62.5FPS communication to the LEDs. 57 | - Currently, the paths for LED images and profiles are absolute. It would be useful if the image paths were relative to the location of the profile. This'd allow portability of these files between computers. That is, you could bring a USB stick to a friend's house and use your profile and images on their setup. 58 | - Most of the code is portable, but a Linux / Mac OS implementation still need to be investigated and deployed. It would be best if the single code base could work on all of them, so the it would need to account for differences between these platforms. 59 | 60 | ### Quality of Life Improvements 61 | 62 | - Automatically select a default profile upon program start-up. 63 | - Option to select different key inputs for people who want different key mappings (instead of the currently hard-coded A/B/C/D). Save this mapping to the profile too. 64 | - Currently, to connect to a pad, PNG files for LEDs need to be specified. This can be annoying. It could be worth having a set of default LED files. Whether that be simple arrows, or just nothing at all. A drop-down or something similar to select default LED files would also be useful here. 65 | - Maybe make the icon actually display in the title bar and taskbar. It's kind of ugly. 66 | - Minimizing to system tray could also be useful. 67 | - The automatic PNG array rotation should be toggle-able. 68 | 69 | ### Feature Improvements 70 | 71 | - The step detection could be much better. It should be converted to use a delta based step tracking system. It can utilize position tracking to handle foot-switching without removal of previous foot. The pad is also prone to vibration-induced hits. Anti-vibration should help. All of these options should be toggle-able / saved to profiles. 72 | - Currently we just serve static images to the panels. This could be dramatically improved. Color-correction could be applied as the LEDs are non-linear in perceived brightness. GIFs could be interpreted to make use of the pads' animation capability. Automatic scaling to the required 12x12 size would make it easier to use; right now only PNGs that are exactly 12x12px are supported. We could even have our own data format for light 'routines'; 73 | - When firmware updates via USB / UART are made available for I/O and Panel boards, the utility should have an interface to flash a compiled executable to the platform. This however, depends on updates to the electronics and firmware. 74 | - In the future, the pad won't just be 4-panel dependent. So a way to interpret how many panels are connected, and what configuration they're in while updating the GUI to match could dramatically improve flexibility. 75 | - When EEPROM emulation and settings are implemented on IO and panel firmware, we should add a settings screen allowing these values to be toggled. The available settings could be passed back from the devices to generate this GUI, or just hard-coded into the GUI. We could then read back these values from the device. 76 | 77 | ## Release 78 | 79 | The [release](https://github.com/ReflexCreations/python-interface/releases/latest) contains the compiled Windows executable of the latest RE:Flex Configuration tool. 80 | 81 | ## License 82 | 83 | For license details, see LICENSE file -------------------------------------------------------------------------------- /assets/co.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReflexCreations/python-interface/b33d9b50f75fce1cbcc2118ce41dbf387e6b0043/assets/co.png -------------------------------------------------------------------------------- /assets/icon-12x12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReflexCreations/python-interface/b33d9b50f75fce1cbcc2118ce41dbf387e6b0043/assets/icon-12x12.png -------------------------------------------------------------------------------- /assets/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReflexCreations/python-interface/b33d9b50f75fce1cbcc2118ce41dbf387e6b0043/assets/icon.ico -------------------------------------------------------------------------------- /assets/mask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReflexCreations/python-interface/b33d9b50f75fce1cbcc2118ce41dbf387e6b0043/assets/mask.png -------------------------------------------------------------------------------- /assets/pg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReflexCreations/python-interface/b33d9b50f75fce1cbcc2118ce41dbf387e6b0043/assets/pg.png -------------------------------------------------------------------------------- /graph_viewer.py: -------------------------------------------------------------------------------- 1 | from PyQt5 import QtCore, QtGui, QtWidgets 2 | from platform_interface import PlatformInterface 3 | import pyqtgraph 4 | import sys 5 | import time 6 | import threading 7 | 8 | 9 | class Viewer(): 10 | def __init__(self): 11 | super().__init__() 12 | self.update_frame = 0 13 | self.color_wheel = ['r', 'g', 'c', 'y'] 14 | pyqtgraph.setConfigOption('background', QtGui.QColor(42, 42, 42)) 15 | pyqtgraph.setConfigOption('foreground', 'w') 16 | self.graph_widget = pyqtgraph.PlotWidget(enableMenu=False) 17 | self.graph_widget.getPlotItem().hideAxis('bottom') 18 | 19 | self.graph_widget.setMouseEnabled(x=False, y=False) 20 | self.x = list(range(4)) 21 | self.y = list(range(4)) 22 | self.data_line = list(range(4)) 23 | for i in range(0, 4): 24 | self.x[i] = list(range(250)) 25 | self.y[i] = list(range(250)) 26 | self.data_line[i] = pyqtgraph.PlotCurveItem(self.x[i], self.y[i], pen=pyqtgraph.mkPen(self.color_wheel[i % 4], width=2)) 27 | self.graph_widget.addItem(self.data_line[i]) 28 | 29 | 30 | def start_plot(self, platform_interface, panel): 31 | self.offset = panel * 4 32 | self.platform_interface = platform_interface 33 | self.timer = QtCore.QTimer() 34 | self.timer.setInterval(1) 35 | self.timer.timeout.connect(self.update_plot_data) 36 | self.timer.start() 37 | 38 | def stop_plot(self): 39 | self.timer.stop() 40 | 41 | def update_plot_data(self): 42 | if self.platform_interface is not None: 43 | for i in range(0, 4): 44 | self.x[i] = self.x[i][1:] 45 | self.x[i].append(time.time()) 46 | self.y[i] = self.y[i][1:] 47 | self.y[i].append(self.platform_interface.panel_data[i + self.offset]) 48 | 49 | self.update_frame += 1 50 | if (self.update_frame % 4) == 0: 51 | for i in range(0, 4): 52 | self.data_line[i].setData(self.x[i], self.y[i]) 53 | 54 | if __name__ == '__main__': 55 | app = QtWidgets.QApplication(sys.argv) 56 | viewer = Viewer() 57 | sys.exit(app.exec_()) -------------------------------------------------------------------------------- /keyboard_input.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | 3 | PUL = ctypes.POINTER(ctypes.c_ulong) 4 | class KeyBdInput(ctypes.Structure): 5 | _fields_ = [("wVk", ctypes.c_ushort), 6 | ("wScan", ctypes.c_ushort), 7 | ("dwFlags", ctypes.c_ulong), 8 | ("time", ctypes.c_ulong), 9 | ("dwExtraInfo", PUL)] 10 | 11 | class HardwareInput(ctypes.Structure): 12 | _fields_ = [("uMsg", ctypes.c_ulong), 13 | ("wParamL", ctypes.c_short), 14 | ("wParamH", ctypes.c_ushort)] 15 | 16 | class MouseInput(ctypes.Structure): 17 | _fields_ = [("dx", ctypes.c_long), 18 | ("dy", ctypes.c_long), 19 | ("mouseData", ctypes.c_ulong), 20 | ("dwFlags", ctypes.c_ulong), 21 | ("time",ctypes.c_ulong), 22 | ("dwExtraInfo", PUL)] 23 | 24 | class Input_I(ctypes.Union): 25 | _fields_ = [("ki", KeyBdInput), 26 | ("mi", MouseInput), 27 | ("hi", HardwareInput)] 28 | 29 | class Input(ctypes.Structure): 30 | _fields_ = [("type", ctypes.c_ulong), 31 | ("ii", Input_I)] 32 | 33 | class KeyboardInput(): 34 | def __init__(self, data, sensitivities, keymaps): 35 | self.set_baselines(data) 36 | self.key_values = [] 37 | for keymap in keymaps: 38 | self.key_values.append(keymap) 39 | self.thresholds = [sensitivities[0], sensitivities[1], sensitivities[2], sensitivities[3]] 40 | self.hysteresis = [self.thresholds[0]/2, self.thresholds[1]/2, self.thresholds[2]/2, self.thresholds[3]/2] 41 | self.is_pressed = [ 0, 0, 0, 0] 42 | 43 | def set_baselines(self, data): 44 | self.baselines = [] 45 | index = 0 46 | for panel_baseline in data: 47 | self.baselines.append(0) 48 | self.baselines[index] = panel_baseline 49 | index += 1 50 | 51 | def PressKey(self, hexKeyCode): 52 | extra = ctypes.c_ulong(0) 53 | ii_ = Input_I() 54 | ii_.ki = KeyBdInput( 0, hexKeyCode, 0x0008, 0, ctypes.pointer(extra) ) 55 | x = Input( ctypes.c_ulong(1), ii_ ) 56 | ctypes.windll.user32.SendInput(1, ctypes.pointer(x), ctypes.sizeof(x)) 57 | 58 | def ReleaseKey(self, hexKeyCode): 59 | extra = ctypes.c_ulong(0) 60 | ii_ = Input_I() 61 | ii_.ki = KeyBdInput( 0, hexKeyCode, 0x0008 | 0x0002, 0, ctypes.pointer(extra) ) 62 | x = Input( ctypes.c_ulong(1), ii_ ) 63 | ctypes.windll.user32.SendInput(1, ctypes.pointer(x), ctypes.sizeof(x)) 64 | 65 | def poll_keys(self, data): 66 | index = 0 67 | for panel_value in data: 68 | if not self.is_pressed[index]: 69 | if panel_value > (self.baselines[index] + self.thresholds[index]): 70 | self.PressKey(self.key_values[index]) 71 | self.is_pressed[index] = 1 72 | else: 73 | if panel_value < (self.baselines[index] + self.thresholds[index] - self.hysteresis[index]): 74 | self.ReleaseKey(self.key_values[index]) 75 | self.is_pressed[index] = 0 76 | index += 1 -------------------------------------------------------------------------------- /led_processing.py: -------------------------------------------------------------------------------- 1 | from PIL import Image 2 | import numpy as np 3 | import math 4 | 5 | class LedProcessorError(Exception): 6 | pass 7 | 8 | class LedProcessor(): 9 | def __init__(self, image, rot): 10 | """ 11 | Initializes processing of a 12x12 PNG image for reflex Dance LED data 12 | processing. 13 | The image must be 12x12. Grayscale is currently not supported, but 14 | RGB, RGBA at any bit depth is supported. RGB values of non-8 bit depth 15 | will be multiplied to be represented in 8 bit. 16 | Parameters: 17 | - png_image: a 4-tuple returned by pypng's reader.Read 18 | Raises: 19 | - LedProcessorError if the PNG image has dimensions that do not 20 | match 12x12 21 | - LedProcessorError if the PNG image is greyscale. 22 | """ 23 | exp_w = self.__LED_GRID_WIDTH 24 | exp_h = self.__LED_GRID_HEIGHT 25 | 26 | if image.size[0] != self.__LED_GRID_WIDTH or image.size[1] != self.__LED_GRID_HEIGHT: 27 | 28 | raise LedProcessorError( 29 | f"Can only process {exp_w}x{exp_h} PNGs at the moment" 30 | ) 31 | 32 | image = image.rotate(rot) 33 | 34 | image.load() 35 | bg = Image.new("RGB", image.size, (255, 255, 255)) 36 | bg.paste(image, mask=image.split()[3]) 37 | bitmap = np.array(bg.getdata()) 38 | 39 | self.masked_rgb_list = self.__make_masked_rgb_array(bitmap) 40 | 41 | self.__ordered_rgb_list = \ 42 | self.__order_into_segments(self.masked_rgb_list) 43 | 44 | def from_file(png_file_path, rot): 45 | r = Image.open(png_file_path) 46 | return LedProcessor(r, rot) 47 | 48 | __LED_GRID_WIDTH = 12 49 | __LED_GRID_HEIGHT = 12 50 | __LED_COUNT = 84 51 | __LEDS_PER_SEGMENT = 21 52 | 53 | # Pixel mask shows which of the pixels in a 12x12 bitmap we care about. 54 | # In processing the data, only the rgb data for the pixels represented by 55 | # a 1 in this grid is kept, the rest discarded. 56 | __PIXEL_MASK = [ 57 | [0,0,0,0,0,1,1,0,0,0,0,0], 58 | [0,0,0,0,1,1,1,1,0,0,0,0], 59 | [0,0,0,1,1,1,1,1,1,0,0,0], 60 | [0,0,1,1,1,1,1,1,1,1,0,0], 61 | [0,1,1,1,1,1,1,1,1,1,1,0], 62 | [1,1,1,1,1,1,1,1,1,1,1,1], 63 | [1,1,1,1,1,1,1,1,1,1,1,1], 64 | [0,1,1,1,1,1,1,1,1,1,1,0], 65 | [0,0,1,1,1,1,1,1,1,1,0,0], 66 | [0,0,0,1,1,1,1,1,1,0,0,0], 67 | [0,0,0,0,1,1,1,1,0,0,0,0], 68 | [0,0,0,0,0,1,1,0,0,0,0,0] 69 | ] 70 | 71 | # Lookup table for gamme corrections, each of these 256 values corresponds 72 | # to an input R, G, or B value 73 | __GAMMA_LUT = [ 74 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 75 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 76 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 77 | 2, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 5, 5, 5, 78 | 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 79 | 10, 10, 11, 11, 11, 12, 12, 13, 13, 13, 14, 14, 15, 15, 16, 16, 80 | 17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22, 23, 24, 24, 25, 81 | 25, 26, 27, 27, 28, 29, 29, 30, 31, 32, 32, 33, 34, 35, 35, 36, 82 | 37, 38, 39, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 50, 83 | 51, 52, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 66, 67, 68, 84 | 69, 70, 72, 73, 74, 75, 77, 78, 79, 81, 82, 83, 85, 86, 87, 89, 85 | 90, 92, 93, 95, 96, 98, 99,101,102,104,105,107,109,110,112,114, 86 | 115,117,119,120,122,124,126,127,129,131,133,135,137,138,140,142, 87 | 144,146,148,150,152,154,156,158,160,162,164,167,169,171,173,175, 88 | 177,180,182,184,186,189,191,193,196,198,200,203,205,208,210,213, 89 | 215,218,220,223,225,228,231,233,236,239,241,244,247,249,252,255 90 | ] 91 | 92 | # LED positioning lookup table to translate from regular left to right, 93 | # top to bottom, into left to right, top to bottom, in 4 segments, 94 | # in the order: top-left, top-right, bottom-left, bottom-right 95 | # The index used in this array is the original index, the value stored 96 | # there is where it should go in the repositioned array 97 | __POSITION_LUT = [ 98 | 0, 21, 2, 1, 23, 22, 5, 4, 3, 26, 25, 24, 99 | 9, 8, 7, 6, 30, 29, 28, 27, 14, 13, 12, 11, 100 | 10, 35, 34, 33, 32, 31, 20, 19, 18, 17, 16, 15, 101 | 41, 40, 39, 38, 37, 36, 83, 82, 81, 80, 79, 78, 102 | 62, 61, 60, 59, 58, 57, 77, 76, 75, 74, 73, 56, 103 | 55, 54, 53, 52, 72, 71, 70, 69, 51, 50, 49, 48, 104 | 68, 67, 66, 47, 46, 45, 65, 64, 44, 43, 63, 42 105 | ] 106 | 107 | def get_segment_data(self, segment_index): 108 | start_index = segment_index * self.__LEDS_PER_SEGMENT 109 | end_index = start_index + self.__LEDS_PER_SEGMENT 110 | segment = self.__ordered_rgb_list[start_index : end_index] 111 | 112 | flat_segment = [] 113 | 114 | for led in segment: 115 | r, g, b = led 116 | # LEDs expect GRB not RGB 117 | flat_segment.extend([g,r,b]) 118 | 119 | return flat_segment 120 | 121 | def __make_masked_rgb_array(self, bitmap): 122 | """ 123 | Given the sequence of lines, each containing rgb[a] values of a given 124 | bit depth, applies the pixel mask to only keep the pixels we're 125 | interested in, discards alpha values as transparency doesn't apply, 126 | and returns an array of 3-value (RGB) tuples, representing each of the 127 | LEDs in pixel_mask where it's "1", in order of left to right, top to 128 | bottom. We also apply color correction here. 129 | """ 130 | 131 | rgb_values_list = [] 132 | y = 0 133 | values_per_pixel = 3 134 | 135 | bitmap = bitmap.reshape(12, 36) 136 | 137 | for row in bitmap: 138 | val_index = -1 139 | for val in row: 140 | val_index += 1 141 | x = val_index // values_per_pixel 142 | 143 | # If pixel_mask dictates current pixel is unused, skip to next 144 | if self.__PIXEL_MASK[y][x] == 0: continue 145 | rgb_values_list.append(int(val)) 146 | 147 | y += 1 148 | 149 | rgb_values = [] 150 | 151 | # Group into rgb tuples 152 | for i in range(0, int(len(rgb_values_list) / 3)): 153 | r = self.color_correct(float(rgb_values_list[i * 3 + 0])/255.0, 1.0) 154 | g = self.color_correct(float(rgb_values_list[i * 3 + 1])/255.0, 0.8) 155 | b = self.color_correct(float(rgb_values_list[i * 3 + 2])/255.0, 0.9) 156 | rgb_values.append((r,g,b)) 157 | 158 | return rgb_values 159 | 160 | def color_correct(self, in_col, mul): 161 | out_col = math.floor(math.pow(in_col, 2) * 255.0 * mul) 162 | out_col = max(0, min(out_col, 255)) 163 | out_col = int(out_col) 164 | return out_col 165 | 166 | def __order_into_segments(self, rgb_values): 167 | ordered_leds = [None] * self.__LED_COUNT 168 | 169 | for i, led in enumerate(rgb_values): 170 | ordered_leds[self.__POSITION_LUT[i]] = led 171 | 172 | return ordered_leds -------------------------------------------------------------------------------- /led_viewer.py: -------------------------------------------------------------------------------- 1 | from PyQt5 import QtWidgets, QtGui, QtCore 2 | from pathlib import Path 3 | 4 | """ 5 | This viewer is meant to emulate LED behavior on the pad, however it's currently only partially 6 | implemented for the first release. 7 | """ 8 | 9 | class LedViewer(QtWidgets.QWidget): 10 | def __init__(self): 11 | super().__init__() 12 | self.layout = QtWidgets.QHBoxLayout() 13 | self.layout.setContentsMargins(0, 0, 0, 0) 14 | #self.viewer = LedPanel() 15 | self.settings = LedSettings() 16 | 17 | self.layout.addWidget(self.settings) 18 | #self.layout.addWidget(self.viewer) 19 | self.setLayout(self.layout) 20 | 21 | pos_by_id = [ 22 | [ 0, 5], [ 0, 6], [ 1, 4], [ 1, 5], [ 1, 6], [ 1, 7], [ 2, 3], [ 2, 4], [ 2, 5], [ 2, 6], [ 2, 7], [ 2, 8], 23 | [ 3, 2], [ 3, 3], [ 3, 4], [ 3, 5], [ 3, 6], [ 3, 7], [ 3, 8], [ 3, 9], [ 4, 1], [ 4, 2], [ 4, 3], [ 4, 4], 24 | [ 4, 5], [ 4, 6], [ 4, 7], [ 4, 8], [ 4, 9], [ 4,10], [ 5, 0], [ 5, 1], [ 5, 2], [ 5, 3], [ 5, 4], [ 5, 5], 25 | [ 5, 6], [ 5, 7], [ 5, 8], [ 5, 9], [ 5,10], [ 5,11], [ 6, 0], [ 6, 1], [ 6, 2], [ 6, 3], [ 6, 4], [ 6, 5], 26 | [ 6, 6], [ 6, 7], [ 6, 8], [ 6, 9], [ 6,10], [ 6,11], [ 7, 1], [ 7, 2], [ 7, 3], [ 7, 4], [ 7, 5], [ 7, 6], 27 | [ 7, 7], [ 7, 8], [ 7, 9], [ 7,10], [ 8, 2], [ 8, 3], [ 8, 4], [ 8, 5], [ 8, 6], [ 8, 7], [ 8, 8], [ 8, 9], 28 | [ 9, 3], [ 9, 4], [ 9, 5], [ 9, 6], [ 9, 7], [ 9, 8], [10, 4], [10, 5], [10, 6], [10, 7], [11, 5], [11, 6] 29 | ] 30 | 31 | class LedSettings(QtWidgets.QWidget): 32 | def __init__(self): 33 | super().__init__() 34 | self.layout = QtWidgets.QVBoxLayout() 35 | 36 | self.label = QtWidgets.QLabel("Light Settings") 37 | font = QtGui.QFont() 38 | font.setBold(True) 39 | self.label.setFont(font) 40 | self.layout.addWidget(self.label) 41 | self.layout.setAlignment(QtCore.Qt.AlignTop) 42 | 43 | self.style_select = QtWidgets.QComboBox() 44 | self.style_select.addItem("On Press") 45 | self.style_select.setEnabled(False) 46 | self.layout.addWidget(self.style_select) 47 | self.file_path = "" 48 | 49 | self.file_picker = QtWidgets.QPushButton("Open File") 50 | self.layout.addWidget(self.file_picker) 51 | self.file_picker.clicked.connect(self.get_led_file) 52 | 53 | self.file_label = QtWidgets.QLabel("Selected:") 54 | self.file_name_label = QtWidgets.QLabel() 55 | self.layout.addWidget(self.file_label) 56 | self.layout.addWidget(self.file_name_label) 57 | 58 | self.setLayout(self.layout) 59 | 60 | def get_led_file(self): 61 | self.dialog = QtWidgets.QFileDialog() 62 | self.dialog.setFileMode(QtWidgets.QFileDialog.ExistingFile) 63 | self.dialog.setNameFilters(["Image Files(*.png *.bmp)"]) 64 | if self.dialog.exec_(): 65 | file_name = self.dialog.selectedFiles() 66 | self.file_path = file_name[0] 67 | f = open(file_name[0], 'r') 68 | with f: 69 | self.file_name_label.setText(Path(file_name[0]).stem) 70 | 71 | def set_led_path(self, path): 72 | try: 73 | f = open(path, 'r') 74 | with f: 75 | self.file_path = path 76 | self.file_name_label.setText(Path(path).stem) 77 | except Exception: 78 | pass 79 | 80 | class LedPanel(QtWidgets.QWidget): 81 | def __init__(self): 82 | super().__init__() 83 | self.layout = QtWidgets.QGridLayout() 84 | self.layout.setVerticalSpacing(0) 85 | self.layout.setHorizontalSpacing(0) 86 | self.LED_SIZE = 12 87 | self.AREA = (self.LED_SIZE + 2) * 10 + 6 88 | for coord in pos_by_id: 89 | led = LedEmulator(self.LED_SIZE) 90 | self.layout.addWidget(led, coord[0], coord[1]) 91 | self.setLayout(self.layout) 92 | self.setFixedWidth(self.AREA) 93 | self.setFixedHeight(self.AREA) 94 | 95 | class LedEmulator(QtWidgets.QWidget): 96 | def __init__(self, led_size): 97 | super().__init__() 98 | self.LED_SIZE = led_size 99 | 100 | def paintEvent(self, event): 101 | qp = QtGui.QPainter() 102 | qp.begin(self) 103 | self.draw_led(qp) 104 | qp.end() 105 | 106 | def draw_led(self, qp): 107 | brush = QtGui.QBrush(QtCore.Qt.SolidPattern) 108 | pen = QtGui.QPen(1) 109 | pen.setColor(QtGui.QColor(QtGui.QColor(42, 42, 42))) 110 | qp.setBrush(brush) 111 | qp.setPen(pen) 112 | qp.drawRect(0, 0, self.LED_SIZE, self.LED_SIZE) 113 | -------------------------------------------------------------------------------- /main_window.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import json 3 | from PyQt5 import QtWidgets, QtGui, QtCore 4 | from platform_interface import PlatformInterface 5 | from graph_viewer import Viewer 6 | from led_viewer import LedViewer 7 | from sensor_viewer import SensorViewer 8 | from pathlib import Path 9 | 10 | 11 | class MainWindow(QtWidgets.QMainWindow): 12 | def __init__(self): 13 | self.settings = QtCore.QSettings("RE:Flex", "Dance Pad Settings") 14 | self.profile = {} 15 | super(MainWindow, self).__init__() 16 | self.load_settings() 17 | title_string = "RE:Flex Configuration" 18 | self.setWindowTitle(title_string) 19 | self.setPalette(self.dark_palette()) 20 | 21 | scroll_area = QtWidgets.QScrollArea() 22 | layout = QtWidgets.QVBoxLayout() 23 | widget = QtWidgets.QWidget() 24 | widget.setLayout(layout) 25 | 26 | toolbar = self.toolbar() 27 | self.panel_interfaces = [] 28 | left_panel = PanelInterface("Left") 29 | self.panel_interfaces.append(left_panel) 30 | down_panel = PanelInterface("Down") 31 | self.panel_interfaces.append(down_panel) 32 | up_panel = PanelInterface("Up") 33 | self.panel_interfaces.append(up_panel) 34 | right_panel = PanelInterface("Right") 35 | self.panel_interfaces.append(right_panel) 36 | layout.addWidget(toolbar) 37 | layout.addWidget(left_panel.widget) 38 | layout.addWidget(down_panel.widget) 39 | layout.addWidget(up_panel.widget) 40 | layout.addWidget(right_panel.widget) 41 | scroll_area.setWidget(widget) 42 | scroll_area.setWidgetResizable(True) 43 | scroll_area.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) 44 | 45 | self.setCentralWidget(scroll_area) 46 | 47 | self.platform = PlatformInterface() 48 | self.enumerate() 49 | 50 | self.update_timer = QtCore.QTimer() 51 | self.update_timer.timeout.connect(self.widget_update) 52 | 53 | def dark_palette(self): 54 | palette = QtGui.QPalette() 55 | palette.setColor(QtGui.QPalette.Window, QtGui.QColor(53, 53, 53)) 56 | palette.setColor(QtGui.QPalette.WindowText, QtCore.Qt.white) 57 | palette.setColor(QtGui.QPalette.Base, QtGui.QColor(25, 25, 25)) 58 | palette.setColor(QtGui.QPalette.AlternateBase, QtGui.QColor(53, 53, 53)) 59 | palette.setColor(QtGui.QPalette.ToolTipBase, QtCore.Qt.black) 60 | palette.setColor(QtGui.QPalette.ToolTipText, QtCore.Qt.white) 61 | palette.setColor(QtGui.QPalette.Text, QtCore.Qt.white) 62 | palette.setColor(QtGui.QPalette.Button, QtGui.QColor(53, 53, 53)) 63 | palette.setColor(QtGui.QPalette.ButtonText, QtCore.Qt.white) 64 | palette.setColor(QtGui.QPalette.BrightText, QtCore.Qt.red) 65 | palette.setColor(QtGui.QPalette.Link, QtGui.QColor(42, 130, 218)) 66 | palette.setColor(QtGui.QPalette.Highlight, QtGui.QColor(42, 130, 218)) 67 | palette.setColor(QtGui.QPalette.HighlightedText, QtCore.Qt.black) 68 | palette.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.Dark, QtCore.Qt.black) 69 | palette.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.Shadow, QtCore.Qt.black) 70 | palette.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.Base, QtGui.QColor(49, 49, 49)) 71 | palette.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.Text, QtGui.QColor(90, 90, 90)) 72 | palette.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.Button, QtGui.QColor(42, 42, 42)) 73 | palette.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.ButtonText, QtGui.QColor(90, 90, 90)) 74 | palette.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.Window, QtGui.QColor(49, 49, 49)) 75 | palette.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.WindowText, QtGui.QColor(90, 90, 90)) 76 | return palette 77 | 78 | def toolbar(self): 79 | self.available_pads = QtWidgets.QComboBox() 80 | self.pad_rename = QtWidgets.QPushButton("Rename") 81 | self.pad_rename.setFixedWidth(80) 82 | self.pad_rename.clicked.connect(self.rename_pad) 83 | self.connect_button = QtWidgets.QPushButton("Connect") 84 | self.connect_button.setFixedWidth(80) 85 | self.connect_button.clicked.connect(self.connect_clicked) 86 | self.disconnect_button = QtWidgets.QPushButton("Disconnect") 87 | self.disconnect_button.setFixedWidth(80) 88 | self.disconnect_button.setDisabled(True) 89 | self.disconnect_button.clicked.connect(self.disconnect_clicked) 90 | self.enumerate_button = QtWidgets.QPushButton("Refresh") 91 | self.enumerate_button.setFixedWidth(80) 92 | self.enumerate_button.clicked.connect(self.enumerate) 93 | 94 | connect_toolbar_layout = QtWidgets.QHBoxLayout() 95 | connect_toolbar_layout.addWidget(self.available_pads) 96 | connect_toolbar_layout.addWidget(self.pad_rename) 97 | connect_toolbar_layout.addWidget(self.connect_button) 98 | connect_toolbar_layout.addWidget(self.disconnect_button) 99 | connect_toolbar_layout.addWidget(self.enumerate_button) 100 | connect_toolbar_widget = QtWidgets.QWidget() 101 | connect_toolbar_widget.setLayout(connect_toolbar_layout) 102 | connect_toolbar_layout.setContentsMargins(0, 0, 0, 0) 103 | 104 | self.open_profile_button = QtWidgets.QPushButton("Open Profile") 105 | self.open_profile_button.setFixedWidth(120) 106 | self.open_profile_button.clicked.connect(self.get_profile) 107 | self.save_profile_button = QtWidgets.QPushButton("Save Profile") 108 | self.save_profile_button.setFixedWidth(120) 109 | self.save_profile_button.clicked.connect(self.set_profile) 110 | self.show_graph = QtWidgets.QCheckBox("Update Graphs") 111 | self.show_graph.setFixedWidth(95) 112 | fps_font = QtGui.QFont() 113 | fps_font.setBold(True) 114 | self.sensor_freq = QtWidgets.QLabel("Sensor Freq: 0000 Hz") 115 | self.sensor_freq.setFont(fps_font) 116 | self.sensor_freq.setFixedWidth(120) 117 | self.lights_freq = QtWidgets.QLabel("LED FPS: 00") 118 | self.lights_freq.setFont(fps_font) 119 | self.lights_freq.setFixedWidth(90) 120 | 121 | options_toolbar_layout = QtWidgets.QHBoxLayout() 122 | options_toolbar_layout.addWidget(self.open_profile_button) 123 | options_toolbar_layout.addWidget(self.save_profile_button) 124 | options_toolbar_layout.addWidget(self.show_graph) 125 | options_toolbar_layout.addWidget(self.sensor_freq) 126 | options_toolbar_layout.addWidget(self.lights_freq) 127 | options_toolbar_layout.addStretch() 128 | options_toolbar_widget = QtWidgets.QWidget() 129 | options_toolbar_widget.setLayout(options_toolbar_layout) 130 | options_toolbar_layout.setContentsMargins(2, 2, 2, 2) 131 | 132 | toolbar_layout = QtWidgets.QVBoxLayout() 133 | toolbar_layout.setContentsMargins(2, 2, 2, 2) 134 | 135 | toolbar_layout.addWidget(connect_toolbar_widget) 136 | toolbar_layout.addWidget(options_toolbar_widget) 137 | toolbar_widget = QtWidgets.QWidget() 138 | toolbar_widget.setLayout(toolbar_layout) 139 | return toolbar_widget 140 | 141 | def rename_pad(self): 142 | text, ok = QtWidgets.QInputDialog.getText(self, "Rename Pad", "Serial " + self.available_pads.currentData() + ": ") 143 | if ok: 144 | self.available_pads.setItemText(self.available_pads.currentIndex(), text) 145 | 146 | def load_profile(self): 147 | self.show_graph.setChecked(self.profile['update_graphs']) 148 | i = 0 149 | for interface in self.panel_interfaces: 150 | interface.sensor_viewer.settings.sensitivity_selector.setValue(self.profile['sensitivities'][i]) 151 | index = interface.sensor_viewer.settings.key_selector.findData(self.profile['keymaps'][i]) 152 | if index is not -1: 153 | interface.sensor_viewer.settings.key_selector.setCurrentIndex(index) 154 | interface.led_viewer.settings.set_led_path(self.profile['light_paths'][i]) 155 | i += 1 156 | self.available_pads.setCurrentIndex(self.available_pads.findData(self.profile['selected_profile'])) 157 | 158 | def get_profile(self): 159 | self.profile_picker = QtWidgets.QFileDialog() 160 | self.profile_picker.setFileMode(QtWidgets.QFileDialog.ExistingFile) 161 | self.profile_picker.setNameFilters(["RE:Flex Profile Files(*.rfx)"]) 162 | if self.profile_picker.exec_(): 163 | file_n = self.profile_picker.selectedFiles() 164 | f = open(file_n[0], 'r') 165 | with f: 166 | file_name = Path(file_n[0]).stem 167 | title_string = "RE:Flex Configuration - " + file_name 168 | self.setWindowTitle(title_string) 169 | self.profile = json.load(f) 170 | self.load_profile() 171 | 172 | def set_profile(self): 173 | path, _ = QtWidgets.QFileDialog.getSaveFileName(self, filter='RE:Flex Profile Files(*.rfx)') 174 | if path != '': 175 | with open(path, 'w', encoding='utf-8') as f: 176 | file_name = Path(path).stem 177 | self.profile['name'] = file_name 178 | self.profile['selected_profile'] = self.available_pads.currentData() 179 | self.profile['update_graphs'] = self.show_graph.isChecked() 180 | self.profile['sensitivities'] = [] 181 | self.profile['light_paths'] = [] 182 | self.profile['keymaps'] = [] 183 | for interface in self.panel_interfaces: 184 | self.profile['sensitivities'].append(interface.sensor_viewer.settings.sensitivity) 185 | self.profile['keymaps'].append(interface.sensor_viewer.settings.keymap) 186 | self.profile['light_paths'].append(interface.led_viewer.settings.file_path) 187 | json.dump(self.profile, f, indent=4) 188 | title_string = "RE:Flex Configuration - " + str(file_name) 189 | self.setWindowTitle(title_string) 190 | 191 | def connect_clicked(self): 192 | led_files = [] 193 | for interface in self.panel_interfaces: 194 | led_files.append(interface.led_viewer.settings.file_path) 195 | try: 196 | self.platform.assign_led_files(led_files) 197 | except Exception: 198 | QtWidgets.QMessageBox.warning(self, "Warning", "Please select valid 12*12px PNG files for each panel.") 199 | return 200 | 201 | sensitivities = [] 202 | keymaps = [] 203 | for interface in self.panel_interfaces: 204 | sensitivities.append(interface.sensor_viewer.settings.sensitivity) 205 | keymaps.append(interface.sensor_viewer.settings.keymap) 206 | 207 | if not self.platform.launch(self.available_pads.currentData(), sensitivities, keymaps): 208 | QtWidgets.QMessageBox.warning(self, "Warning", "Cannot open connection to selected dance pad.") 209 | self.enumerate() 210 | elif self.platform.is_running: 211 | self.update_timer.start(1000) 212 | self.connect_button.setDisabled(True) 213 | self.enumerate_button.setDisabled(True) 214 | self.disconnect_button.setEnabled(True) 215 | self.pad_rename.setDisabled(True) 216 | self.open_profile_button.setDisabled(True) 217 | self.save_profile_button.setDisabled(True) 218 | self.show_graph.setDisabled(True) 219 | self.available_pads.setDisabled(True) 220 | for interface in self.panel_interfaces: 221 | interface.sensor_viewer.settings.sensitivity_selector.setDisabled(True) 222 | interface.sensor_viewer.settings.key_selector.setDisabled(True) 223 | interface.led_viewer.settings.file_picker.setDisabled(True) 224 | panel_index = 0 225 | if self.show_graph.isChecked(): 226 | for panel_interface in self.panel_interfaces: 227 | panel_interface.viewer.start_plot(self.platform, panel_index) 228 | panel_index += 1 229 | 230 | def disconnect_clicked(self): 231 | self.platform.stop_loop() 232 | self.update_timer.stop() 233 | panel_index = 0 234 | if self.show_graph.isChecked(): 235 | for panel_interface in self.panel_interfaces: 236 | panel_interface.viewer.stop_plot() 237 | panel_index += 1 238 | self.sensor_freq.setText("Sensor Freq: 0000 Hz") 239 | self.lights_freq.setText("LED FPS: 00") 240 | 241 | self.connect_button.setEnabled(True) 242 | self.pad_rename.setEnabled(True) 243 | self.enumerate_button.setEnabled(True) 244 | self.disconnect_button.setDisabled(True) 245 | self.open_profile_button.setEnabled(True) 246 | self.save_profile_button.setEnabled(True) 247 | self.show_graph.setEnabled(True) 248 | self.available_pads.setEnabled(True) 249 | for interface in self.panel_interfaces: 250 | interface.sensor_viewer.settings.sensitivity_selector.setEnabled(True) 251 | interface.sensor_viewer.settings.key_selector.setEnabled(True) 252 | interface.led_viewer.settings.file_picker.setEnabled(True) 253 | 254 | def enumerate(self): 255 | for i in range(self.available_pads.count()): 256 | self.settings.setValue(self.available_pads.itemData(i), self.available_pads.itemText(i)) 257 | devs = self.platform.enumerate() 258 | self.available_pads.clear() 259 | for d in devs: 260 | if d['serial_number'] in self.settings.allKeys(): 261 | name = self.settings.value(d['serial_number']) 262 | else: 263 | name = d['serial_number'] 264 | self.available_pads.addItem(name, d['serial_number']) 265 | self.available_pads.setCurrentIndex(0) 266 | if len(self.available_pads) == 0: 267 | self.connect_button.setDisabled(True) 268 | self.pad_rename.setDisabled(True) 269 | else: 270 | self.connect_button.setEnabled(True) 271 | self.pad_rename.setEnabled(True) 272 | 273 | def load_settings(self): 274 | if not self.settings.value("geometry") is None: 275 | self.restoreGeometry(self.settings.value("geometry")) 276 | if not self.settings.value("windowState") is None: 277 | self.restoreState(self.settings.value("windowState")) 278 | 279 | self.setMinimumWidth(640) 280 | self.setMinimumHeight(480) 281 | 282 | def save_settings(self): 283 | self.settings.setValue("geometry", self.saveGeometry()) 284 | self.settings.setValue("windowState", self.saveState()) 285 | for i in range(self.available_pads.count()): 286 | self.settings.setValue(self.available_pads.itemData(i), self.available_pads.itemText(i)) 287 | 288 | def closeEvent(self, event): 289 | self.save_settings() 290 | 291 | def widget_update(self): 292 | self.sensor_freq.setText("Sensor Freq: " + "{:04d}".format(self.platform.sensor_rate()) + "Hz") 293 | self.lights_freq.setText("LED FPS: " + "{:04d}".format(self.platform.lights_rate())) 294 | 295 | 296 | class PanelInterface(): 297 | def __init__(self, name): 298 | panel_layout = QtWidgets.QHBoxLayout() 299 | self.viewer = Viewer() 300 | panel_layout.addWidget(self.viewer.graph_widget) 301 | self.led_panel = QtWidgets.QWidget() 302 | self.led_viewer = LedViewer() 303 | self.sensor_viewer = SensorViewer() 304 | panel_layout.addWidget(self.led_viewer) 305 | panel_layout.addWidget(self.sensor_viewer) 306 | 307 | self.widget = QtWidgets.QGroupBox(name) 308 | self.widget.setLayout(panel_layout) 309 | 310 | 311 | if __name__ == "__main__": 312 | app = QtWidgets.QApplication(sys.argv) 313 | app.setStyle("Fusion") 314 | window = MainWindow() 315 | window.show() 316 | app.exec_() 317 | -------------------------------------------------------------------------------- /platform_interface.py: -------------------------------------------------------------------------------- 1 | import hid 2 | import threading 3 | from led_processing import LedProcessor 4 | import numpy as np 5 | from keyboard_input import KeyboardInput 6 | 7 | class PlatformInterface(): 8 | def enumerate(self): 9 | devices = [d for d in hid.enumerate(self.USB_VID, self.USB_PID)] 10 | return devices 11 | 12 | def launch(self, serial, sensitivities, keymaps): 13 | if serial == None: 14 | serial = '0' 15 | self.h = hid.device() 16 | try: 17 | self.h.open(self.USB_VID, self.USB_PID, serial_number=serial) 18 | except Exception: 19 | return 0 20 | if self.h.get_product_string() == 'RE:Flex Dance Pad': 21 | self.is_running = True 22 | self.setup(sensitivities, keymaps) 23 | return 1 24 | else: 25 | self.is_running = False 26 | return 0 27 | 28 | def assign_led_files(self, led_files): 29 | self.led_files = led_files 30 | self.led_sources = [ 31 | LedProcessor.from_file(self.led_files[0], 90), 32 | LedProcessor.from_file(self.led_files[1], 180), 33 | LedProcessor.from_file(self.led_files[2], 0), 34 | LedProcessor.from_file(self.led_files[3], 270) 35 | ] 36 | 37 | def setup(self, sensitivities, keymaps): 38 | self.sample_counter = 0 39 | 40 | data = self.h.read(64) 41 | self.organize_data(data) 42 | self.sum_panel_data(self.panel_data) 43 | self.keyboard_input = KeyboardInput(self.panel_values, sensitivities, keymaps) 44 | 45 | self.pressed_on_frame = list(range(4)) 46 | self.last_frame = list(range(4)) 47 | self.led_frame = 0 48 | self.led_panel = 0 49 | self.led_segment = 0 50 | self.led_frame_data = 0 51 | self.led_data = [] 52 | self.lights_counter = 0 53 | 54 | thread = threading.Thread(target=self.loop, daemon=True) 55 | thread.start() 56 | 57 | def loop(self): 58 | while self.is_running: 59 | data = self.h.read(64) 60 | self.organize_data(data) 61 | self.sum_panel_data(self.panel_data) 62 | self.keyboard_input.poll_keys(self.panel_values) 63 | self.sample_counter += 1 64 | 65 | self.update_led_frame() 66 | self.h.write(bytes(self.led_data)) 67 | 68 | def update_led_frame(self): 69 | self.led_frame_data = 0 70 | if self.led_segment < 3: 71 | self.led_segment += 1 72 | else: 73 | self.led_segment = 0 74 | if self.led_panel < 3: 75 | self.led_panel += 1 76 | else: 77 | self.led_panel = 0 78 | if self.led_frame < 15: 79 | self.led_frame += 1 80 | else: 81 | self.led_frame = 0 82 | if self.led_frame != self.last_frame[self.led_panel]: 83 | self.lights_counter += 1 84 | if self.keyboard_input.is_pressed[self.led_panel]: 85 | self.pressed_on_frame[self.led_panel] = 1 86 | else: 87 | self.pressed_on_frame[self.led_panel] = 0 88 | self.last_frame[self.led_panel] = self.led_frame 89 | 90 | self.led_frame_data |= self.led_panel << 6 91 | self.led_frame_data |= self.led_segment << 4 92 | self.led_frame_data |= self.led_frame 93 | 94 | source = self.led_sources[self.led_panel] 95 | segment_data = source.get_segment_data(self.led_segment) 96 | if self.pressed_on_frame[self.led_panel]: 97 | self.led_data = [0, self.led_frame_data] + segment_data 98 | else: 99 | self.led_data = [0, self.led_frame_data] + [0 for i in range(63)] 100 | 101 | def sensor_rate(self): 102 | polling_rate = self.sample_counter 103 | self.sample_counter = 0 104 | return polling_rate 105 | 106 | def lights_rate(self): 107 | polling_rate = self.lights_counter // 4 108 | self.lights_counter = 0 109 | return polling_rate 110 | 111 | def stop_loop(self): 112 | self.is_running = False 113 | 114 | def sum_panel_data(self, panel_data): 115 | self.panel_values = [] 116 | for panel in range(0, 4): 117 | self.panel_values.append(0) 118 | for sensor in range(0, 4): 119 | self.panel_values[panel] += self.panel_data[sensor + 4 * panel] 120 | 121 | def organize_data(self, data): 122 | self.panel_data = [] 123 | for i in range(0, 32): 124 | self.panel_data.append(0) 125 | data_index = 0 126 | for data_point in data: 127 | if data_index % 2 == 0: 128 | self.panel_data[data_index // 2] = data_point 129 | if data_index % 2 == 1: 130 | self.panel_data[data_index // 2] |= 0x0FFF & (data_point << 8) 131 | data_index += 1 132 | 133 | USB_VID = 0x0483 # Vendor ID for I/O Microcontroller 134 | USB_PID = 0x5750 # Product ID for I/O Microcontroller 135 | panel_data = [] 136 | 137 | if __name__ == "__main__": 138 | pf = PlatformInterface() 139 | -------------------------------------------------------------------------------- /profiles/Brittany_Stamina.rfx: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Brittany_Stamina", 3 | "selected_profile": "2063345A4641", 4 | "update_graphs": false, 5 | "sensitivities": [ 6 | 200, 7 | 300, 8 | 100, 9 | 200 10 | ], 11 | "light_paths": [ 12 | "C:/Users/Brittany/repos/reflex-2-release/python-interface/assets/co.png", 13 | "C:/Users/Brittany/repos/reflex-2-release/python-interface/assets/pg.png", 14 | "C:/Users/Brittany/repos/reflex-2-release/python-interface/assets/pg.png", 15 | "C:/Users/Brittany/repos/reflex-2-release/python-interface/assets/co.png" 16 | ] 17 | } -------------------------------------------------------------------------------- /profiles/Brittany_Test.rfx: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Brittany_Test", 3 | "selected_profile": "2063345A4641", 4 | "update_graphs": true, 5 | "sensitivities": [ 6 | 600, 7 | 600, 8 | 600, 9 | 600 10 | ], 11 | "keymaps": [ 12 | 30, 13 | 48, 14 | 46, 15 | 32 16 | ], 17 | "light_paths": [ 18 | "C:/Users/brittany/repos/reflex/python-interface/assets/co.png", 19 | "C:/Users/brittany/repos/reflex/python-interface/assets/pg.png", 20 | "C:/Users/brittany/repos/reflex/python-interface/assets/pg.png", 21 | "C:/Users/brittany/repos/reflex/python-interface/assets/co.png" 22 | ] 23 | } -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PyQt5 2 | hidapi 3 | pillow 4 | numpy 5 | pyqtgraph 6 | pyinstaller -------------------------------------------------------------------------------- /sensor_viewer.py: -------------------------------------------------------------------------------- 1 | from PyQt5 import QtWidgets, QtGui, QtCore 2 | 3 | 4 | class SensorViewer(QtWidgets.QWidget): 5 | def __init__(self): 6 | super().__init__() 7 | self.layout = QtWidgets.QHBoxLayout() 8 | self.layout.setContentsMargins(0, 0, 0, 0) 9 | self.settings = SensorSettings() 10 | self.layout.addWidget(self.settings) 11 | self.setLayout(self.layout) 12 | 13 | 14 | class SensorSettings(QtWidgets.QWidget): 15 | def __init__(self): 16 | super().__init__() 17 | self.layout = QtWidgets.QVBoxLayout() 18 | self.sensitivity = 10 19 | self.keymap = 30 20 | self.scan_code_dict = { 21 | "A": 30, 22 | "B": 48, 23 | "C": 46, 24 | "D": 32, 25 | "E": 18, 26 | "F": 33, 27 | "G": 34, 28 | "H": 35, 29 | "I": 23, 30 | "J": 36, 31 | "K": 37, 32 | "L": 38, 33 | "M": 50, 34 | "N": 49, 35 | "O": 24, 36 | "P": 25, 37 | "Q": 16, 38 | "R": 19, 39 | "S": 31, 40 | "T": 20, 41 | "U": 22, 42 | "V": 47, 43 | "W": 17, 44 | "X": 45, 45 | "Y": 21, 46 | "Z": 44 47 | } 48 | 49 | self.label = QtWidgets.QLabel("Sensor Settings") 50 | font = QtGui.QFont() 51 | font.setBold(True) 52 | self.label.setFont(font) 53 | self.layout.addWidget(self.label) 54 | self.layout.setAlignment(QtCore.Qt.AlignTop) 55 | 56 | sensitivity_label = QtWidgets.QLabel("Panel Sensitivity:") 57 | self.sensitivity_selector = QtWidgets.QSpinBox() 58 | self.sensitivity_selector.setRange(10, 1000) 59 | self.sensitivity_selector.valueChanged.connect(self.update_sensitivity) 60 | 61 | key_label = QtWidgets.QLabel("Key mapped:") 62 | self.key_selector = QtWidgets.QComboBox() 63 | for key, value in self.scan_code_dict.items(): 64 | self.key_selector.addItem(key, value) 65 | self.key_selector.currentIndexChanged.connect(self.update_keymap) 66 | 67 | self.layout.addWidget(sensitivity_label) 68 | self.layout.addWidget(self.sensitivity_selector) 69 | self.layout.addWidget(key_label) 70 | self.layout.addWidget(self.key_selector) 71 | 72 | self.setLayout(self.layout) 73 | 74 | def update_sensitivity(self): 75 | self.sensitivity = self.sensitivity_selector.value() 76 | 77 | def update_keymap(self): 78 | self.keymap = self.key_selector.currentData() 79 | --------------------------------------------------------------------------------