├── .gitignore ├── LICENSE ├── README.md ├── boot.py ├── code.py ├── config.py ├── controls.py ├── direct_input.py ├── leds.py ├── lib ├── adafruit_hid │ ├── __init__.mpy │ ├── consumer_control.mpy │ ├── consumer_control_code.mpy │ ├── keyboard.mpy │ ├── keyboard_layout_us.mpy │ ├── keycode.mpy │ └── mouse.mpy └── neopixel.mpy ├── misc └── cover.png └── ofs.py /.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | .vscode/.browse.c_cpp.db* 3 | .vscode/c_cpp_properties.json 4 | .vscode/launch.json 5 | .vscode/ipch 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Jonathan Barket 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Logo 4 | 5 | 6 | [![Contributors][contributors-shield]][contributors-url] 7 | [![Forks][forks-shield]][forks-url] 8 | [![Stargazers][stars-shield]][stars-url] 9 | [![Issues][issues-shield]][issues-url] 10 | [![MIT License][license-shield]][license-url] 11 | 12 |

13 | OpenStickFirmware is open source software designed to handle any and all tasks required in a custom Fight Stick. It can handle being the brains of your entire stick, or just handling the bells and whistles while your Brook board talks to your console. 14 |
15 |
16 | Explore the Docs 17 | · 18 | Report Bug 19 | · 20 | Request Feature 21 |

22 |

23 | 24 | 25 | ## About The Project 26 | 27 | OSF is built with CircuitPython and intended to run on _any_ board it supports. Not only does this make the customization process friendlier for non-developers, but it also means a significant amount of power can be had for a mere four dollars. 28 | 29 | 30 | ## Getting Started 31 | 32 | Once you're installed CircuitPython on your device of choice, setting up OSF is as simple as downloading the files from this repository and placing them on the drive that appears on your computer. 33 | 34 | There's no need to compile anything or download additional software. 35 | 36 | ### Configuration 37 | Basic configuration is handled in _config.py_. In here, you can configure whether or not your board has WS2812b (or similar) LEDs, which pins each button is connected to and so on. 38 | 39 | Currently, the only _required_ buttons are left, down, right and up. Anything you don't need can just be commented out. 40 | 41 | 42 | ## Roadmap 43 | 44 | See the [open issues](https://github.com/SleepUnit/OpenStickFirmware/issues) for a list of proposed features (and known issues). 45 | 46 | 47 | 48 | ## Contributing 49 | 50 | Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. 51 | 52 | 1. Fork the Project 53 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) 54 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) 55 | 4. Push to the Branch (`git push origin feature/AmazingFeature`) 56 | 5. Open a Pull Request 57 | 58 | 59 | 60 | ## License 61 | 62 | Distributed under the MIT License. See `LICENSE` for more information. 63 | 64 | 65 | 66 | ## Contact 67 | 68 | Jonathan Barket - jbarket@sleepunit.com 69 | 70 | Project Link: [https://github.com/SleepUnit/OpenStickFirmware](https://github.com/SleepUnit/OpenStickFirmware) 71 | 72 | 73 | 74 | 75 | 76 | [contributors-shield]: https://img.shields.io/github/contributors/SleepUnit/OpenStickFirmware.svg?style=for-the-badge 77 | [contributors-url]: https://github.com/SleepUnit/OpenStickFirmware/graphs/contributors 78 | [forks-shield]: https://img.shields.io/github/forks/SleepUnit/OpenStickFirmware.svg?style=for-the-badge 79 | [forks-url]: https://github.com/SleepUnit/OpenStickFirmware/network/members 80 | [stars-shield]: https://img.shields.io/github/stars/SleepUnit/OpenStickFirmware.svg?style=for-the-badge 81 | [stars-url]: https://github.com/SleepUnit/OpenStickFirmware/stargazers 82 | [issues-shield]: https://img.shields.io/github/issues/SleepUnit/OpenStickFirmware.svg?style=for-the-badge 83 | [issues-url]: https://github.com/SleepUnit/OpenStickFirmware/repo/issues 84 | [license-shield]: https://img.shields.io/github/license/SleepUnit/OpenStickFirmware.svg?style=for-the-badge 85 | [license-url]: https://github.com/SleepUnit/OpenStickFirmware/blob/main/LICENSE.txt -------------------------------------------------------------------------------- /boot.py: -------------------------------------------------------------------------------- 1 | import board 2 | import usb_hid 3 | from direct_input import DirectInput 4 | 5 | # Add Appropriate HID Descriptor. 6 | direct_input = DirectInput() 7 | devices = [direct_input.device()] 8 | usb_hid.enable(devices) -------------------------------------------------------------------------------- /code.py: -------------------------------------------------------------------------------- 1 | import board 2 | import usb_hid 3 | import neopixel 4 | from ofs import OpenFightStick 5 | from controls import Button, Directional 6 | from leds import Leds 7 | from config import config 8 | 9 | # Setup LEDs if the appropriate configuration exists. Otherwise, 10 | # make sure we set pixels so that we don't have to do bootleg 11 | # Python undefined checking everywhere. 12 | pixels = None 13 | 14 | if config.get('led_pin') and config.get('led_num'): 15 | pixels = neopixel.NeoPixel(config.get('led_pin'), config.get('led_num'), brightness=0.3, auto_write=False) 16 | 17 | # Add buttons and their corresponding LEDs, if they exist in our config. 18 | def add_button(pin, hid_num, leds): 19 | if config.get(pin): 20 | buttons.append(Button(config.get(pin), hid_num, pixels, config.get(leds, []))) 21 | 22 | buttons = [] 23 | add_button('1p_pin', 0, '1p_leds') 24 | add_button('2p_pin', 3, '2p_leds') 25 | add_button('3p_pin', 5, '3p_leds') 26 | add_button('4p_pin', 4, '4p_leds') 27 | add_button('1k_pin', 1, '1k_leds') 28 | add_button('2k_pin', 2, '2k_leds') 29 | add_button('3k_pin', 7, '3k_leds') 30 | add_button('4k_pin', 6, '4k_leds') 31 | add_button('select_pin', 8, 'select_leds') 32 | add_button('start_pin', 9, 'start_leds') 33 | add_button('home_pin', 12, 'home_leds') 34 | add_button('l3_pin', 10, 'l3_leds') 35 | add_button('r3_pin', 11, 'r3_leds') 36 | add_button('touch_pin', 13, 'touch_leds') 37 | 38 | # left, down, right, up 39 | dpad = Directional(config.get('left_pin'), config.get('down_pin'), config.get('right_pin'), config.get('up_pin'), pixels, config.get('left_leds', []), config.get('down_leds', []), config.get('right_leds', []), config.get('up_leds', [])) 40 | ofs = OpenFightStick(usb_hid.devices) 41 | 42 | while True: 43 | for button in buttons: 44 | if button.pressed(): 45 | ofs.press_button(button) 46 | button.start_animation() 47 | else: 48 | ofs.release_button(button) 49 | button.animate() 50 | 51 | ofs.move_hat(dpad.dpad()) 52 | dpad.animate() 53 | if pixels: 54 | pixels.show() 55 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import board 2 | 3 | config = { 4 | # Have LEDs? Specify the pin and number of LEDs. These should be WS2812b or 5 | # other NeoPixel style LEDs. Comment these lines out otherwise. 6 | "led_pin": board.GP14, 7 | "led_num": 16, 8 | 9 | # Action Buttons. Specify the pin for any buttons your controller has. Comment out 10 | # any buttons you don't have. If your controller has LEDs for each button, also 11 | # provide the index of those. This may be one or many depending on how much you 12 | # live that RGB life. 13 | "1p_pin": board.GP0, 14 | "1p_leds": [4], 15 | "2p_pin": board.GP1, 16 | "2p_leds": [5], 17 | "3p_pin": board.GP2, 18 | "3p_leds": [6], 19 | "4p_pin": board.GP3, 20 | "4p_leds": [7], 21 | 22 | "1k_pin": board.GP4, 23 | "1k_leds": [8], 24 | "2k_pin": board.GP5, 25 | "2k_leds": [9], 26 | "3k_pin": board.GP6, 27 | "3k_leds": [10], 28 | "4k_pin": board.GP7, 29 | "4k_leds": [11], 30 | 31 | "select_pin": board.GP8, 32 | # "select_leds": [12], 33 | "start_pin": board.GP9, 34 | # "start_leds": [9], 35 | "home_pin": board.GP28, 36 | # "home_leds": [10], 37 | "l3_pin": board.GP17, 38 | # "l3_leds": [11], 39 | "r3_pin": board.GP16, 40 | # "r3_leds": [11], 41 | "touch_pin": board.GP18, 42 | # "touch_leds": [11], 43 | 44 | # Directional Buttons. More of the same. Currently, these pins are required. That's 45 | # probably shortsighted on my part, but it is what it is. 46 | "left_pin": board.GP10, 47 | "left_leds": [0], 48 | "down_pin": board.GP11, 49 | "down_leds": [1], 50 | "right_pin": board.GP12, 51 | "right_leds": [2], 52 | "up_pin": board.GP13, 53 | "up_leds": [3] 54 | } -------------------------------------------------------------------------------- /controls.py: -------------------------------------------------------------------------------- 1 | import digitalio 2 | import board 3 | from leds import Leds 4 | 5 | class Button: 6 | def __init__(self, pin, num, pixels = None, pixel_group = []): 7 | self.num = num 8 | self.pixels = pixels 9 | 10 | self._button = digitalio.DigitalInOut(pin) 11 | self._button.direction = digitalio.Direction.INPUT 12 | self._button.pull = digitalio.Pull.UP 13 | 14 | self.leds = None 15 | 16 | if pixels and len(pixel_group) > 0: 17 | self.leds = Leds(pixels, pixel_group) 18 | 19 | def pressed(self): 20 | return not self._button.value 21 | 22 | def start_animation(self): 23 | if self.leds: 24 | self.leds.start("Wheel") 25 | 26 | def animate(self): 27 | if self.leds: 28 | self.leds.animate() 29 | 30 | 31 | class Directional: 32 | def __init__(self, left_pin, down_pin, right_pin, up_pin, pixels, left_pixel_group, down_pixel_group, right_pixel_group, up_pixel_group): 33 | self.left_leds = None 34 | self.down_leds = None 35 | self.right_leds = None 36 | self.up_leds = None 37 | 38 | if pixels: 39 | if len(left_pixel_group) > 0: 40 | self.left_leds = Leds(pixels, left_pixel_group) 41 | if len(down_pixel_group) > 0: 42 | self.down_leds = Leds(pixels, down_pixel_group) 43 | if len(right_pixel_group) > 0: 44 | self.right_leds = Leds(pixels, right_pixel_group) 45 | if len(up_pixel_group) > 0: 46 | self.up_leds = Leds(pixels, up_pixel_group) 47 | 48 | self._left = digitalio.DigitalInOut(left_pin) 49 | self._left.direction = digitalio.Direction.INPUT 50 | self._left.pull = digitalio.Pull.UP 51 | 52 | self._down = digitalio.DigitalInOut(down_pin) 53 | self._down.direction = digitalio.Direction.INPUT 54 | self._down.pull = digitalio.Pull.UP 55 | 56 | self._right = digitalio.DigitalInOut(right_pin) 57 | self._right.direction = digitalio.Direction.INPUT 58 | self._right.pull = digitalio.Pull.UP 59 | 60 | self._up = digitalio.DigitalInOut(up_pin) 61 | self._up.direction = digitalio.Direction.INPUT 62 | self._up.pull = digitalio.Pull.UP 63 | 64 | 65 | def animate(self): 66 | if self.left_leds: 67 | self.left_leds.animate() 68 | if self.down_leds: 69 | self.down_leds.animate() 70 | if self.right_leds: 71 | self.right_leds.animate() 72 | if self.up_leds: 73 | self.up_leds.animate() 74 | 75 | def dpad(self): 76 | return self._full_neutral_clean(not self._left.value, not self._down.value, not self._right.value, not self._up.value) 77 | 78 | def _full_neutral_clean(self, left = False, down = False, right = False, up = False): 79 | if up and down: 80 | up = False 81 | down = False 82 | 83 | if left and right: 84 | left = False 85 | right = False 86 | 87 | return self._post_clean(left, down, right, up) 88 | 89 | def _post_clean(self, left = False, down = False, right = False, up = False): 90 | if right and up: 91 | if self.right_leds: 92 | self.right_leds.start("Wheel") 93 | if self.up_leds: 94 | self.up_leds.start("Wheel") 95 | return 1 96 | 97 | if right and down: 98 | if self.right_leds: 99 | self.right_leds.start("Wheel") 100 | if self.down_leds: 101 | self.down_leds.start("Wheel") 102 | return 3 103 | 104 | if right: 105 | if self.right_leds: 106 | self.right_leds.start("Wheel") 107 | return 2 108 | 109 | if left and up: 110 | if self.left_leds: 111 | self.left_leds.start("Wheel") 112 | if self.up_leds: 113 | self.up_leds.start("Wheel") 114 | return 7 115 | 116 | if left and down: 117 | if self.left_leds: 118 | self.left_leds.start("Wheel") 119 | if self.down_leds: 120 | self.down_leds.start("Wheel") 121 | return 5 122 | 123 | if left: 124 | if self.left_leds: 125 | self.left_leds.start("Wheel") 126 | return 6 127 | 128 | if up: 129 | if self.up_leds: 130 | self.up_leds.start("Wheel") 131 | return 0 132 | 133 | if down: 134 | if self.down_leds: 135 | self.down_leds.start("Wheel") 136 | return 4 137 | 138 | return 8 -------------------------------------------------------------------------------- /direct_input.py: -------------------------------------------------------------------------------- 1 | import usb_hid 2 | 3 | class DirectInput: 4 | def __init__(self): 5 | self.report_id = 7 6 | 7 | def descriptor(self): 8 | return bytes(( 9 | 0x05, 0x01, # USAGE_PAGE (Generic Desktop) 10 | 0x09, 0x05, # USAGE (Gamepad) - Very important for Switch 11 | 0xa1, 0x01, # COLLECTION (Application) 12 | 0x85, 0xFF, # 7 [SET AT RUNTIME] 13 | 14 | # 16 Buttons 15 | 0x05, 0x09, # USAGE_PAGE (Button) 16 | 0x19, 0x01, # USAGE_MINIMUM (Button 1) 17 | 0x29, 0x10, # USAGE_MAXIMUM (Button 16) 18 | 0x15, 0x00, # LOGICAL_MINIMUM (0) 19 | 0x25, 0x01, # LOGICAL_MAXIMUM (1) 20 | 0x75, 0x01, # REPORT_SIZE (1) 21 | 0x95, 0x10, # REPORT_COUNT (16) 22 | 0x55, 0x00, # UNIT_EXPONENT (0) 23 | 0x65, 0x00, # UNIT (None) 24 | 0x81, 0x02, # INPUT (Data,Var,Abs) 25 | 26 | # One Hat switches (8 Positions) 27 | 0x05, 0x01, # USAGE_PAGE (Generic Desktop) 28 | 0x09, 0x39, # USAGE (Hat switch) 29 | 0x15, 0x00, # LOGICAL_MINIMUM (0) 30 | 0x25, 0x07, # LOGICAL_MAXIMUM (7) 31 | 0x35, 0x00, # PHYSICAL_MINIMUM (0) 32 | 0x46, 0x3B, 0x01, # PHYSICAL_MAXIMUM (315) 33 | 0x65, 0x14, # UNIT (Eng Rot:Angular Pos) 34 | 0x75, 0x04, # REPORT_SIZE (4) 35 | 0x95, 0x01, # REPORT_COUNT (1) 36 | 0x81, 0x02, # INPUT (Data,Var,Abs) 37 | 38 | 0x65, 0x00, 39 | 0x95, 0x01, 40 | 0x81, 0x01, 41 | 42 | # X, Y, and Z Axis 43 | 0x15, 0x00, # LOGICAL_MINIMUM (0) 44 | 0x26, 0xff, 0x00, # LOGICAL_MAXIMUM (255) 45 | 0x75, 0x08, # REPORT_SIZE (8) 46 | 0x09, 0x01, # USAGE (Pointer) 47 | 0xA1, 0x00, # COLLECTION (Physical) 48 | 0x09, 0x30, # USAGE (x) 49 | 0x09, 0x31, # USAGE (y) 50 | 0x09, 0x32, # USAGE (z) 51 | 0x09, 0x35, # USAGE (rz) 52 | 0x95, 0x04, # REPORT_COUNT (4) 53 | 0x81, 0x02, # INPUT (Data,Var,Abs) 54 | 0xc0, # END_COLLECTION 55 | 0xc0 # END_COLLECTION 56 | )) 57 | 58 | def device(self): 59 | return usb_hid.Device( 60 | report_descriptor = self.descriptor(), 61 | usage_page = 0x1, 62 | usage = 0x5, 63 | in_report_length = 7, 64 | out_report_length = 1, 65 | report_id_index = self.report_id, 66 | ) 67 | 68 | 69 | -------------------------------------------------------------------------------- /leds.py: -------------------------------------------------------------------------------- 1 | import time 2 | import board 3 | import neopixel 4 | 5 | RED = (255, 0, 0) 6 | YELLOW = (255, 150, 0) 7 | GREEN = (0, 255, 0) 8 | CYAN = (0, 255, 255) 9 | BLUE = (0, 0, 255) 10 | PURPLE = (180, 0, 255) 11 | 12 | WHEEL_FRAMES = 64 13 | WHEEL_TOTAL_TIME = 1.4 14 | WHEEL_FRAME_TIME = (WHEEL_TOTAL_TIME) / WHEEL_FRAMES 15 | 16 | class Leds: 17 | def __init__(self, pixels, pixel_group): 18 | self.pixels = pixels 19 | self.pixel_group = pixel_group 20 | self.reset() 21 | 22 | def reset(self): 23 | self.current_frame = 0 24 | self.last_frame_time = None 25 | self.animation = None 26 | self.color((0, 0, 0)) 27 | 28 | def start(self, animation): 29 | if self.current_frame < 1: 30 | self.current_frame = 0 31 | self.last_frame_time = None 32 | self.animation = animation 33 | else: 34 | self.current_frame = 0 35 | 36 | def color(self, color): 37 | for pixel in self.pixel_group: 38 | self.pixels[pixel] = color 39 | 40 | def animate(self): 41 | if self.animation == "Wheel": 42 | self.wheel() 43 | 44 | def wheel(self): 45 | if self.current_frame > WHEEL_FRAMES: 46 | self.reset() 47 | return 48 | 49 | if self.last_frame_time == None or time.monotonic() - self.last_frame_time > WHEEL_FRAME_TIME: 50 | self.color(self.wheel_position()) 51 | self.last_frame_time = time.monotonic() 52 | self.current_frame = self.current_frame + 1 53 | 54 | def wheel_position(self): 55 | conversion = 255 / WHEEL_FRAMES 56 | pos = self.current_frame * conversion 57 | 58 | if pos < 0 or pos > 255: 59 | return (0, 0, 0) 60 | if pos < 85: 61 | return (255 - pos * 3, pos * 3, 0) 62 | if pos < 170: 63 | pos -= 85 64 | return (0, 255 - pos * 3, pos * 3) 65 | pos -= 170 66 | return (pos * 3, 0, 255 - pos * 3) 67 | 68 | -------------------------------------------------------------------------------- /lib/adafruit_hid/__init__.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SleepUnit/OpenStickFirmware/9a8fcb8543a04dfb85c545fdd276072b56a631f8/lib/adafruit_hid/__init__.mpy -------------------------------------------------------------------------------- /lib/adafruit_hid/consumer_control.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SleepUnit/OpenStickFirmware/9a8fcb8543a04dfb85c545fdd276072b56a631f8/lib/adafruit_hid/consumer_control.mpy -------------------------------------------------------------------------------- /lib/adafruit_hid/consumer_control_code.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SleepUnit/OpenStickFirmware/9a8fcb8543a04dfb85c545fdd276072b56a631f8/lib/adafruit_hid/consumer_control_code.mpy -------------------------------------------------------------------------------- /lib/adafruit_hid/keyboard.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SleepUnit/OpenStickFirmware/9a8fcb8543a04dfb85c545fdd276072b56a631f8/lib/adafruit_hid/keyboard.mpy -------------------------------------------------------------------------------- /lib/adafruit_hid/keyboard_layout_us.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SleepUnit/OpenStickFirmware/9a8fcb8543a04dfb85c545fdd276072b56a631f8/lib/adafruit_hid/keyboard_layout_us.mpy -------------------------------------------------------------------------------- /lib/adafruit_hid/keycode.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SleepUnit/OpenStickFirmware/9a8fcb8543a04dfb85c545fdd276072b56a631f8/lib/adafruit_hid/keycode.mpy -------------------------------------------------------------------------------- /lib/adafruit_hid/mouse.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SleepUnit/OpenStickFirmware/9a8fcb8543a04dfb85c545fdd276072b56a631f8/lib/adafruit_hid/mouse.mpy -------------------------------------------------------------------------------- /lib/neopixel.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SleepUnit/OpenStickFirmware/9a8fcb8543a04dfb85c545fdd276072b56a631f8/lib/neopixel.mpy -------------------------------------------------------------------------------- /misc/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SleepUnit/OpenStickFirmware/9a8fcb8543a04dfb85c545fdd276072b56a631f8/misc/cover.png -------------------------------------------------------------------------------- /ofs.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Jonathan Barket 2 | # Copyright (c) 2018 Dan Halbert for Adafruit Industries 3 | 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | # SOFTWARE. 21 | 22 | from adafruit_hid.keyboard import find_device 23 | import usb_hid 24 | import struct 25 | import time 26 | 27 | class OpenFightStick: 28 | def __init__(self, devices): 29 | self._device = find_device(devices, usage_page=0x1, usage=0x05) 30 | 31 | # report[0] buttons 1-8 (LSB is button 1) 32 | # report[1] buttons 9-16 33 | # report[2] hat 0-8 34 | # report[3] joystick 0 x: -127 to 127 35 | # report[4] joystick 0 y: -127 to 127 36 | # report[5] joystick 1 x: -127 to 127 37 | # report[6] joystick 1 y: -127 to 127 38 | self._report = bytearray(7) 39 | 40 | # Remember the last report as well, so we can avoid sending 41 | # duplicate reports. 42 | self._last_report = bytearray(7) 43 | 44 | # Store settings separately before putting into report. Saves code 45 | # especially for buttons. 46 | self._buttons_state = 0 47 | self._hat_position = 8 48 | self._joy_x = 0 49 | self._joy_y = 0 50 | self._joy_z = 0 51 | self._joy_r_z = 0 52 | 53 | # Send an initial report to test if HID device is ready. 54 | # If not, wait a bit and try once more. 55 | try: 56 | self.reset_all() 57 | except OSError: 58 | time.sleep(1) 59 | self.reset_all() 60 | 61 | def press_button(self, button): 62 | self._buttons_state |= 1 << button.num 63 | self._send() 64 | 65 | def release_button(self, button): 66 | self._buttons_state &= ~(1 << button.num) 67 | self._send() 68 | 69 | def move_hat(self, direction): 70 | """ DPad is interpreted as a hat. Values are 0-7 where N is 0, and the 71 | directions move clockwise. 8 is centered. """ 72 | self._hat_position = direction 73 | self._send() 74 | 75 | def move_joysticks(self, x=None, y=None, z=None, r_z=None): 76 | if x is not None: 77 | self._joy_x = x 78 | if y is not None: 79 | self._joy_y = y 80 | if z is not None: 81 | self._joy_z = z 82 | if r_z is not None: 83 | self._joy_r_z = r_z 84 | self._send() 85 | 86 | def reset_all(self): 87 | """Return the fightstick to a neutral state""" 88 | self._buttons_state = 0 89 | self._hat_position = 8 90 | self._joy_x = 0 91 | self._joy_y = 0 92 | self._joy_z = 0 93 | self._joy_r_z = 0 94 | self._send(always=True) 95 | 96 | def _send(self, always=False): 97 | """Send a report with all the existing settings. 98 | If ``always`` is ``False`` (the default), send only if there have been changes. 99 | """ 100 | struct.pack_into( 101 | "