mirror of
https://github.com/MickGyver/DaemonBite-Retro-Controllers-USB
synced 2024-10-31 15:45:07 -04:00
Merge 465b39977c
into 362c9d136d
This commit is contained in:
commit
8addc49c81
194
3DOControllersUSB/3DOControllersUSB.ino
Normal file
194
3DOControllersUSB/3DOControllersUSB.ino
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
/* DaemonBite 3DO Controllers to USB Adapter
|
||||||
|
*
|
||||||
|
* Based on DaemonBite NES / SNES Adapter by Mikael Norrgård <mick@daemonbite.com>
|
||||||
|
*
|
||||||
|
* Author: Chris Chaplin
|
||||||
|
*
|
||||||
|
* Copyright (c) 2022 Chris Chaplin
|
||||||
|
*
|
||||||
|
* GNU GENERAL PUBLIC LICENSE
|
||||||
|
* Version 3, 29 June 2007
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
// The bulk of the information on this comes from http://kaele.com/~kashima/games/3do-e.html
|
||||||
|
// and https://github.com/fluxcorenz/UPCB/blob/master/3do.h
|
||||||
|
//
|
||||||
|
// Controller DB9 pins (looking face-on to the end of the plug):
|
||||||
|
//
|
||||||
|
// 5 4 3 2 1
|
||||||
|
// 9 8 7 6
|
||||||
|
//
|
||||||
|
// Wire it all up according to the following table:
|
||||||
|
//
|
||||||
|
// 3DO-DB9 Arduino Pro Micro
|
||||||
|
// ---------------------------------------------
|
||||||
|
// 1 GND GND
|
||||||
|
// 2 VCC VCC
|
||||||
|
// 3 Audio.1(1v-pp)
|
||||||
|
// 4 Audio.1(1v-pp)
|
||||||
|
// 5 VCC VCC
|
||||||
|
// 6 LATCH 2 (PD1)
|
||||||
|
// 7 CLOCK 3 (PD0)
|
||||||
|
// 8 GND GND
|
||||||
|
// 9 DATA A0 (PF7)
|
||||||
|
|
||||||
|
#include "Gamepad.h"
|
||||||
|
|
||||||
|
// ATT: 20 chars max (including NULL at the end) according to Arduino source code.
|
||||||
|
// Additionally serial number is used to differentiate arduino projects to have different button maps!
|
||||||
|
const char *gp_serial = "3DO to USB";
|
||||||
|
|
||||||
|
//#define DEBUG
|
||||||
|
|
||||||
|
#define GAMEPAD_COUNT 1 // NOTE: No more than ONE gamepad is possible at the moment due to the author only having one pad to test with. 3DO gamepads are daisychained (see linked info) so this will need more work to support.
|
||||||
|
#define GAMEPAD_COUNT_MAX 1 // NOTE: Currently set to 1 due to the above. The loops from the (S)NES code to support more than one pad have been left in but don't do anything.
|
||||||
|
#define BUTTON_COUNT 11 // Panasonic FZ-JP1 controller has seven buttons and four axes, totalling 11
|
||||||
|
#define BUTTON_READ_DELAY 88 // Delay between button reads in µs (11 buttons x 8µs clock cycle). Any less than this and buttons reset themselves
|
||||||
|
#define MICROS_CLOCK 4
|
||||||
|
|
||||||
|
#define BUTTONS 0
|
||||||
|
#define AXES 1
|
||||||
|
#define UP 0x01
|
||||||
|
#define DOWN 0x02
|
||||||
|
#define LEFT 0x04
|
||||||
|
#define RIGHT 0x08
|
||||||
|
|
||||||
|
// Set up USB HID gamepads
|
||||||
|
Gamepad_ Gamepad[GAMEPAD_COUNT];
|
||||||
|
|
||||||
|
// Controller
|
||||||
|
uint8_t buttons[GAMEPAD_COUNT_MAX][2] = {{0,0}};
|
||||||
|
uint8_t buttonsPrev[GAMEPAD_COUNT_MAX][2] = {{0,0}};
|
||||||
|
uint8_t gpBit[GAMEPAD_COUNT_MAX] = {B10000000};
|
||||||
|
uint8_t btnByte[BUTTON_COUNT] = {1,1,1,1,0,0,0,0,0,0,0};
|
||||||
|
uint8_t btnBits[BUTTON_COUNT] = {UP,DOWN,LEFT,RIGHT,0x01,0x02,0x04,0x08,0x10,0x20,0x40};
|
||||||
|
uint8_t gp = 0;
|
||||||
|
uint8_t buttonCount = 11; // Panasonic FZ-JP1 has seven buttons and four axes, totalling 11
|
||||||
|
|
||||||
|
// Timing
|
||||||
|
uint32_t microsButtons = 0;
|
||||||
|
|
||||||
|
#ifdef DEBUG
|
||||||
|
uint32_t microsStart = 0;
|
||||||
|
uint32_t microsEnd = 0;
|
||||||
|
uint8_t counter = 0;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
void setup()
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// Setup latch and clock pins (2,3 or PD1, PD0)
|
||||||
|
DDRD = B00000011; // Sets bits 0&1 as outputs
|
||||||
|
PORTD = B00000010; // Clock (PD0) low, Latch (PD1) high
|
||||||
|
|
||||||
|
// Setup data pins (A0)
|
||||||
|
DDRF &= ~B00000000; // inputs
|
||||||
|
PORTF |= B10000000; // enable internal pull-ups
|
||||||
|
|
||||||
|
#ifdef DEBUG
|
||||||
|
Serial.begin(115200);
|
||||||
|
delay(2000);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Short delay to let controllers stabilize
|
||||||
|
delay(200);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() { while(1)
|
||||||
|
{
|
||||||
|
// See if enough time has passed since last button read
|
||||||
|
if((micros() - microsButtons) > BUTTON_READ_DELAY)
|
||||||
|
{
|
||||||
|
|
||||||
|
// Reset the button and axes state
|
||||||
|
for(gp=0; gp<GAMEPAD_COUNT; gp++) {
|
||||||
|
buttons[gp][BUTTONS] = 0;
|
||||||
|
buttons[gp][AXES] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the Clock and Latch lines in the right state for the controller to start sending data
|
||||||
|
pullClock(); //pull clock line high ready it can be dropped in sync with the latch
|
||||||
|
dropLatch(); //drop latch low, controller will start sending data
|
||||||
|
|
||||||
|
// We don't care about the first two (three?) bits from the controller
|
||||||
|
uint8_t clockpulse = 0;
|
||||||
|
while(clockpulse<4) {
|
||||||
|
sendClock();
|
||||||
|
clockpulse++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop through the number of configured buttons and sample the pin state for each
|
||||||
|
for(uint8_t btn=0; btn<BUTTON_COUNT; btn++)
|
||||||
|
{
|
||||||
|
for(gp=0; gp<GAMEPAD_COUNT; gp++) {
|
||||||
|
if((PINF & gpBit[gp])==0) {
|
||||||
|
buttons[gp][btnByte[btn]] |= btnBits[btn];
|
||||||
|
}
|
||||||
|
sendClock(); // Send a clock pulse and loop around to get the next button until we've got them all
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finished getting button and axes data so pull the latch back up
|
||||||
|
pullLatch();
|
||||||
|
|
||||||
|
// Set the USB gamepad based on the button and axes data gathered from the data line
|
||||||
|
for(gp=0; gp<GAMEPAD_COUNT; gp++)
|
||||||
|
{
|
||||||
|
// Has any buttons changed state?
|
||||||
|
// Note checked added for BUTTONS = B00000000 to avoid all buttons reporting as pressed if controller disconnected
|
||||||
|
if (buttons[gp][BUTTONS] != B00000000 && (buttons[gp][BUTTONS] != buttonsPrev[gp][BUTTONS] || buttons[gp][AXES] != buttonsPrev[gp][AXES]))
|
||||||
|
{
|
||||||
|
Gamepad[gp]._GamepadReport.buttons = ~buttons[gp][BUTTONS]; // 3DO controller buttons are low when pressed, so invert
|
||||||
|
Gamepad[gp]._GamepadReport.Y = ((buttons[gp][AXES] & DOWN) >> 1) - (buttons[gp][AXES] & UP);
|
||||||
|
Gamepad[gp]._GamepadReport.X = ((buttons[gp][AXES] & RIGHT) >> 3) - ((buttons[gp][AXES] & LEFT) >> 2);
|
||||||
|
buttonsPrev[gp][BUTTONS] = buttons[gp][BUTTONS];
|
||||||
|
buttonsPrev[gp][AXES] = buttons[gp][AXES];
|
||||||
|
Gamepad[gp].send();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
microsButtons = micros();
|
||||||
|
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
|
||||||
|
void dropLatch()
|
||||||
|
{
|
||||||
|
PORTD &= ~B00000010; // Set LOW PD1
|
||||||
|
}
|
||||||
|
|
||||||
|
void pullLatch()
|
||||||
|
{
|
||||||
|
PORTD |= B00000010; // Set HIGH PD1
|
||||||
|
}
|
||||||
|
|
||||||
|
void pullClock()
|
||||||
|
{
|
||||||
|
PORTD |= B00000001; // Set HIGH PD0
|
||||||
|
delayMicroseconds(MICROS_CLOCK);
|
||||||
|
}
|
||||||
|
|
||||||
|
void sendClock()
|
||||||
|
{
|
||||||
|
// Send a clock pulse to the 3DO controller
|
||||||
|
PORTD &= ~B00000001; // Set LOW PD0
|
||||||
|
delayMicroseconds(MICROS_CLOCK);
|
||||||
|
PORTD |= B00000001; // Set HIGH PD0
|
||||||
|
delayMicroseconds(MICROS_CLOCK);
|
||||||
|
}
|
156
3DOControllersUSB/Gamepad.cpp
Normal file
156
3DOControllersUSB/Gamepad.cpp
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
/* Gamepad.cpp
|
||||||
|
*
|
||||||
|
* Based on the advanced HID library for Arduino:
|
||||||
|
* https://github.com/NicoHood/HID
|
||||||
|
* Copyright (c) 2014-2015 NicoHood
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020 Mikael Norrgård <http://daemonbite.com>
|
||||||
|
*
|
||||||
|
* GNU GENERAL PUBLIC LICENSE
|
||||||
|
* Version 3, 29 June 2007
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
#include "Gamepad.h"
|
||||||
|
|
||||||
|
static const uint8_t _hidReportDescriptor[] PROGMEM = {
|
||||||
|
0x05, 0x01, // USAGE_PAGE (Generic Desktop)
|
||||||
|
0x09, 0x04, // USAGE (Joystick) (Maybe change to gamepad? I don't think so but...)
|
||||||
|
0xa1, 0x01, // COLLECTION (Application)
|
||||||
|
0xa1, 0x00, // COLLECTION (Physical)
|
||||||
|
|
||||||
|
0x05, 0x09, // USAGE_PAGE (Button)
|
||||||
|
0x19, 0x01, // USAGE_MINIMUM (Button 1)
|
||||||
|
0x29, 0x07, // USAGE_MAXIMUM (Button 7)
|
||||||
|
0x15, 0x00, // LOGICAL_MINIMUM (0)
|
||||||
|
0x25, 0x01, // LOGICAL_MAXIMUM (1)
|
||||||
|
0x95, 0x07, // REPORT_COUNT (7)
|
||||||
|
0x75, 0x01, // REPORT_SIZE (1)
|
||||||
|
0x81, 0x02, // INPUT (Data,Var,Abs)
|
||||||
|
|
||||||
|
0x95, 0x01, // REPORT_COUNT (1) ; pad out the bits into a number divisible by 8
|
||||||
|
0x75, 0x01, // REPORT_SIZE (4)
|
||||||
|
0x81, 0x03, // INPUT (Const,Var,Abs)
|
||||||
|
|
||||||
|
0x05, 0x01, // USAGE_PAGE (Generic Desktop)
|
||||||
|
0x09, 0x01, // USAGE (pointer)
|
||||||
|
0xa1, 0x00, // COLLECTION (Physical)
|
||||||
|
0x09, 0x30, // USAGE (X)
|
||||||
|
0x09, 0x31, // USAGE (Y)
|
||||||
|
0x15, 0xff, // LOGICAL_MINIMUM (-1)
|
||||||
|
0x25, 0x01, // LOGICAL_MAXIMUM (1)
|
||||||
|
0x95, 0x02, // REPORT_COUNT (2)
|
||||||
|
0x75, 0x08, // REPORT_SIZE (8)
|
||||||
|
0x81, 0x02, // INPUT (Data,Var,Abs)
|
||||||
|
0xc0, // END_COLLECTION
|
||||||
|
|
||||||
|
0xc0, // END_COLLECTION
|
||||||
|
0xc0, // END_COLLECTION
|
||||||
|
};
|
||||||
|
|
||||||
|
Gamepad_::Gamepad_(void) : PluggableUSBModule(1, 1, epType), protocol(HID_REPORT_PROTOCOL), idle(1)
|
||||||
|
{
|
||||||
|
epType[0] = EP_TYPE_INTERRUPT_IN;
|
||||||
|
PluggableUSB().plug(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
int Gamepad_::getInterface(uint8_t* interfaceCount)
|
||||||
|
{
|
||||||
|
*interfaceCount += 1; // uses 1
|
||||||
|
HIDDescriptor hidInterface = {
|
||||||
|
D_INTERFACE(pluggedInterface, 1, USB_DEVICE_CLASS_HUMAN_INTERFACE, HID_SUBCLASS_NONE, HID_PROTOCOL_NONE),
|
||||||
|
D_HIDREPORT(sizeof(_hidReportDescriptor)),
|
||||||
|
D_ENDPOINT(USB_ENDPOINT_IN(pluggedEndpoint), USB_ENDPOINT_TYPE_INTERRUPT, USB_EP_SIZE, 0x01)
|
||||||
|
};
|
||||||
|
return USB_SendControl(0, &hidInterface, sizeof(hidInterface));
|
||||||
|
}
|
||||||
|
|
||||||
|
int Gamepad_::getDescriptor(USBSetup& setup)
|
||||||
|
{
|
||||||
|
// Check if this is a HID Class Descriptor request
|
||||||
|
if (setup.bmRequestType != REQUEST_DEVICETOHOST_STANDARD_INTERFACE) { return 0; }
|
||||||
|
if (setup.wValueH != HID_REPORT_DESCRIPTOR_TYPE) { return 0; }
|
||||||
|
|
||||||
|
// In a HID Class Descriptor wIndex cointains the interface number
|
||||||
|
if (setup.wIndex != pluggedInterface) { return 0; }
|
||||||
|
|
||||||
|
// Reset the protocol on reenumeration. Normally the host should not assume the state of the protocol
|
||||||
|
// due to the USB specs, but Windows and Linux just assumes its in report mode.
|
||||||
|
protocol = HID_REPORT_PROTOCOL;
|
||||||
|
|
||||||
|
return USB_SendControl(TRANSFER_PGM, _hidReportDescriptor, sizeof(_hidReportDescriptor));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Gamepad_::setup(USBSetup& setup)
|
||||||
|
{
|
||||||
|
if (pluggedInterface != setup.wIndex) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t request = setup.bRequest;
|
||||||
|
uint8_t requestType = setup.bmRequestType;
|
||||||
|
|
||||||
|
if (requestType == REQUEST_DEVICETOHOST_CLASS_INTERFACE)
|
||||||
|
{
|
||||||
|
if (request == HID_GET_REPORT) {
|
||||||
|
// TODO: HID_GetReport();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (request == HID_GET_PROTOCOL) {
|
||||||
|
// TODO: Send8(protocol);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestType == REQUEST_HOSTTODEVICE_CLASS_INTERFACE)
|
||||||
|
{
|
||||||
|
if (request == HID_SET_PROTOCOL) {
|
||||||
|
protocol = setup.wValueL;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (request == HID_SET_IDLE) {
|
||||||
|
idle = setup.wValueL;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (request == HID_SET_REPORT)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Gamepad_::reset()
|
||||||
|
{
|
||||||
|
_GamepadReport.X = 0;
|
||||||
|
_GamepadReport.Y = 0;
|
||||||
|
_GamepadReport.buttons = 0;
|
||||||
|
this->send();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Gamepad_::send()
|
||||||
|
{
|
||||||
|
USB_Send(pluggedEndpoint | TRANSFER_RELEASE, &_GamepadReport, sizeof(GamepadReport));
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t Gamepad_::getShortName(char *name)
|
||||||
|
{
|
||||||
|
if(!next)
|
||||||
|
{
|
||||||
|
strcpy(name, gp_serial);
|
||||||
|
return strlen(name);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
60
3DOControllersUSB/Gamepad.h
Normal file
60
3DOControllersUSB/Gamepad.h
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
/* Gamepad.h
|
||||||
|
*
|
||||||
|
* Based on the advanced HID library for Arduino:
|
||||||
|
* https://github.com/NicoHood/HID
|
||||||
|
* Copyright (c) 2014-2015 NicoHood
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020 Mikael Norrgård <http://daemonbite.com>
|
||||||
|
*
|
||||||
|
* GNU GENERAL PUBLIC LICENSE
|
||||||
|
* Version 3, 29 June 2007
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "HID.h"
|
||||||
|
|
||||||
|
extern const char* gp_serial;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t buttons;
|
||||||
|
int8_t X;
|
||||||
|
int8_t Y;
|
||||||
|
} GamepadReport;
|
||||||
|
|
||||||
|
|
||||||
|
class Gamepad_ : public PluggableUSBModule
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
uint8_t reportId;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
int getInterface(uint8_t* interfaceCount);
|
||||||
|
int getDescriptor(USBSetup& setup);
|
||||||
|
uint8_t getShortName(char *name);
|
||||||
|
bool setup(USBSetup& setup);
|
||||||
|
|
||||||
|
uint8_t epType[1];
|
||||||
|
uint8_t protocol;
|
||||||
|
uint8_t idle;
|
||||||
|
|
||||||
|
public:
|
||||||
|
GamepadReport _GamepadReport;
|
||||||
|
Gamepad_(void);
|
||||||
|
void reset(void);
|
||||||
|
void send();
|
||||||
|
};
|
37
3DOControllersUSB/README.md
Normal file
37
3DOControllersUSB/README.md
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# DaemonBite 3DO USB Controller adapter
|
||||||
|
## Introduction
|
||||||
|
With this simple to build adapter you can connect a 3DO gamepad (specifically tested using a Panasonic FZ-JP1 controller) to a PC, Raspberry PI, MiSTer FPGA etc. The Arduino Pro Micro has very low lag when configured as a USB gamepad and it is plug n' play once it has been programmed.
|
||||||
|
|
||||||
|
Only one controller is currently supported.
|
||||||
|
|
||||||
|
The headphone jack is not supported.
|
||||||
|
|
||||||
|
## Parts you need
|
||||||
|
- Arduino Pro Micro (ATMega32U4)
|
||||||
|
- Male DB9 plug
|
||||||
|
- Wire
|
||||||
|
- Heat shrink tube (Ø ~20mm)
|
||||||
|
- Micro USB cable
|
||||||
|
|
||||||
|
## Wiring
|
||||||
|
Male DB9 socket pins (look at the pins in the socket):
|
||||||
|
|
||||||
|
| DB9 |
|
||||||
|
|-----------|
|
||||||
|
|\1 2 3 4 5/|
|
||||||
|
| \6 7 8 9/ |
|
||||||
|
|
||||||
|
3DO-DB9 | Arduino Pro Micro
|
||||||
|
---------------------------|------------------
|
||||||
|
1 GND | GND
|
||||||
|
2 VCC(5v) | VCC
|
||||||
|
3 Audio.1(1v-pp) |
|
||||||
|
4 Audio.1(1v-pp) |
|
||||||
|
5 VCC(5v) | VCC
|
||||||
|
6 P/S (Shift/Load) | 2 (PD1)
|
||||||
|
7 CLK(125KHz) (Shift/Clock)| 3 (PD0)
|
||||||
|
8 GND | GND
|
||||||
|
9 DATA | A0 (PF7)
|
||||||
|
|
||||||
|
## License
|
||||||
|
This project is licensed under the GNU General Public License v3.0.
|
Loading…
Reference in New Issue
Block a user