This commit is contained in:
2026-05-29 01:11:50 +02:00
parent f4e6d925ab
commit 8cb43eb815
40 changed files with 17776 additions and 0 deletions
+149
View File
@@ -0,0 +1,149 @@
---
Language: Cpp
# BasedOnStyle: LLVM
AccessModifierOffset: -2
AlignAfterOpenBracket: Align
AlignConsecutiveMacros: false
AlignConsecutiveAssignments: false
AlignConsecutiveBitFields: false
AlignConsecutiveDeclarations: false
AlignEscapedNewlines: Right
AlignOperands: Align
AlignTrailingComments: true
AllowAllArgumentsOnNextLine: true
AllowAllConstructorInitializersOnNextLine: true
AllowAllParametersOfDeclarationOnNextLine: true
AllowShortEnumsOnASingleLine: true
AllowShortBlocksOnASingleLine: Never
AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: false
AllowShortLambdasOnASingleLine: All
AllowShortIfStatementsOnASingleLine: Never
AllowShortLoopsOnASingleLine: false
AlwaysBreakAfterDefinitionReturnType: None
AlwaysBreakAfterReturnType: None
AlwaysBreakBeforeMultilineStrings: false
AlwaysBreakTemplateDeclarations: true
BinPackArguments: true
BinPackParameters: true
BraceWrapping:
AfterCaseLabel: false
AfterClass: false
AfterControlStatement: Never
AfterEnum: false
AfterFunction: false
AfterNamespace: false
AfterObjCDeclaration: false
AfterStruct: false
AfterUnion: false
AfterExternBlock: false
BeforeCatch: false
BeforeElse: false
BeforeLambdaBody: false
BeforeWhile: false
IndentBraces: false
SplitEmptyFunction: true
SplitEmptyRecord: true
SplitEmptyNamespace: true
BreakBeforeBinaryOperators: None
BreakBeforeBraces: Attach
BreakBeforeInheritanceComma: false
BreakInheritanceList: BeforeColon
BreakBeforeTernaryOperators: true
BreakConstructorInitializersBeforeComma: true
BreakConstructorInitializers: BeforeColon
BreakAfterJavaFieldAnnotations: false
BreakStringLiterals: true
ColumnLimit: 120
CommentPragmas: '^ IWYU pragma:'
CompactNamespaces: false
ConstructorInitializerAllOnOneLineOrOnePerLine: false
ConstructorInitializerIndentWidth: 0
ContinuationIndentWidth: 4
Cpp11BracedListStyle: true
DeriveLineEnding: true
DerivePointerAlignment: false
DisableFormat: false
ExperimentalAutoDetectBinPacking: false
FixNamespaceComments: true
ForEachMacros:
- foreach
- Q_FOREACH
- BOOST_FOREACH
IncludeBlocks: Preserve
IncludeCategories:
- Regex: '^"(llvm|llvm-c|clang|clang-c)/'
Priority: 2
SortPriority: 0
- Regex: '^(<|"(gtest|gmock|isl|json)/)'
Priority: 3
SortPriority: 0
- Regex: '.*'
Priority: 1
SortPriority: 0
IncludeIsMainRegex: '(Test)?$'
IncludeIsMainSourceRegex: ''
IndentCaseLabels: true
IndentCaseBlocks: false
IndentGotoLabels: true
IndentPPDirectives: None
IndentExternBlock: AfterExternBlock
IndentWidth: 2
IndentWrappedFunctionNames: false
InsertTrailingCommas: None
JavaScriptQuotes: Leave
JavaScriptWrapImports: true
KeepEmptyLinesAtTheStartOfBlocks: true
MacroBlockBegin: ''
MacroBlockEnd: ''
MaxEmptyLinesToKeep: 1
NamespaceIndentation: None
ObjCBinPackProtocolList: Auto
ObjCBlockIndentWidth: 2
ObjCBreakBeforeNestedBlockParam: true
ObjCSpaceAfterProperty: false
ObjCSpaceBeforeProtocolList: true
PenaltyBreakAssignment: 2
PenaltyBreakBeforeFirstCallParameter: 19
PenaltyBreakComment: 300
PenaltyBreakFirstLessLess: 120
PenaltyBreakString: 1000
PenaltyBreakTemplateDeclaration: 10
PenaltyExcessCharacter: 1000000
PenaltyReturnTypeOnItsOwnLine: 60
PointerAlignment: Right
ReflowComments: true
SortIncludes: true
SortUsingDeclarations: true
SpaceAfterCStyleCast: false
SpaceAfterLogicalNot: false
SpaceAfterTemplateKeyword: true
SpaceBeforeAssignmentOperators: true
SpaceBeforeCpp11BracedList: false
SpaceBeforeCtorInitializerColon: true
SpaceBeforeInheritanceColon: true
SpaceBeforeParens: ControlStatements
SpaceBeforeRangeBasedForLoopColon: true
SpaceInEmptyBlock: false
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 1
SpacesInAngles: false
SpacesInConditionalStatement: false
SpacesInContainerLiterals: true
SpacesInCStyleCastParentheses: false
SpacesInParentheses: false
SpacesInSquareBrackets: false
SpaceBeforeSquareBrackets: false
Standard: Latest
StatementMacros:
- Q_UNUSED
- QT_REQUIRE_VERSION
TabWidth: 8
UseCRLF: false
UseTab: Never
WhitespaceSensitiveMacros:
- STRINGIZE
- PP_STRINGIZE
- BOOST_PP_STRINGIZE
...
+65
View File
@@ -0,0 +1,65 @@
// This file is part of Necroware's GamePort adapter firmware.
// Copyright (C) 2021 Necroware
//
// 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 <Arduino.h>
/// Class to read analog axis.
///
/// Usually PC joystick axes are specified to have 100 Ohm resistance, but
/// many analog joysticks have old, bad, or just wrong resistors. This ends
/// up in incorrect positions and is especially bad for games, which don't
/// have calibration features. This class takes care of this issue and
/// applies automatic calibration on every read.
template <int ID>
class AnalogAxis {
public:
/// Constructor.
///
/// The initial state of the joystick is considered as middle
/// which is used for autocalibration.
AnalogAxis() {
pinMode(ID, INPUT);
m_mid = analogRead(ID);
m_min = m_mid - 100;
m_max = m_mid + 100;
}
/// Gets the axis state.
///
/// This function automatically recalculates the outer limits and
/// readjusts the position of the joystick.
/// @returns a value between 0 and 1023
uint16_t get() {
const auto value = analogRead(ID);
if (value < m_min) {
m_min = value;
} else if (value > m_max) {
m_max = value;
}
if (value < m_mid) {
return map(value, m_min, m_mid, 1023, 512);
}
return map(value, m_mid, m_max, 511, 0);
}
private:
int m_mid;
int m_min;
int m_max;
};
@@ -0,0 +1,61 @@
// This file is part of Necroware's GamePort adapter firmware.
// Copyright (C) 2021 Necroware
//
// 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 "AnalogAxis.h"
#include "DigitalPin.h"
#include "GamePort.h"
/// A common class for all analog joysticks.
class AnalogJoystick {
public:
/// Gets axis value.
///
/// @param[in] id is the axes ID
/// @returns a value between 0 and 255
uint16_t getAxis(int id) {
switch (id) {
case 0:
return m_axis1.get();
case 1:
return m_axis2.get();
case 2:
return m_axis3.get();
case 3:
return m_axis4.get();
default:
return 0u;
}
}
/// Gets the buttons state as one byte.
///
/// @returns a byte every bit represents a button
byte getButtons() {
return m_button1.isLow() | m_button2.isLow() << 1 | m_button3.isLow() << 2 | m_button4.isLow() << 3;
}
private:
DigitalInput<GamePort<2>::pin> m_button1;
DigitalInput<GamePort<7>::pin> m_button2;
DigitalInput<GamePort<10>::pin> m_button3;
DigitalInput<GamePort<14>::pin> m_button4;
AnalogAxis<GamePort<3>::pin> m_axis1;
AnalogAxis<GamePort<6>::pin> m_axis2;
AnalogAxis<GamePort<11>::pin> m_axis3;
AnalogAxis<GamePort<13>::pin> m_axis4;
};
+104
View File
@@ -0,0 +1,104 @@
// This file is part of Necroware's GamePort adapter firmware.
// Copyright (C) 2021 Necroware
//
// 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 <Arduino.h>
template <size_t Size>
struct Buffer {
static const uint8_t MAX_SIZE{Size};
uint8_t data[MAX_SIZE]{};
uint8_t size{};
};
template <size_t S>
class BufferFillerImpl {
public:
using BufferType = Buffer<S>;
explicit BufferFillerImpl(BufferType& buffer)
: m_buffer(buffer) {}
~BufferFillerImpl() = default;
BufferFillerImpl(const BufferFillerImpl&) = delete;
BufferFillerImpl(BufferFillerImpl&&) = default;
BufferFillerImpl& operator=(const BufferFillerImpl&) = delete;
BufferFillerImpl& operator=(BufferFillerImpl&&) = default;
template <typename T>
BufferFillerImpl& push(const T& bits) {
return push(uint32_t(bits), sizeof(bits) * BITS_PER_BYTE);
}
BufferFillerImpl& push(uint32_t bits, size_t count) {
while(count) {
const auto pushed = fillup(bits, count);
bits >>= pushed;
count -= pushed;
}
return *this;
}
BufferFillerImpl& align() {
if (m_bitsUsed) {
m_buffer.size++;
m_bitsUsed = 0u;
}
return *this;
}
private:
static constexpr auto BITS_PER_BYTE{8u};
BufferType& m_buffer;
size_t m_bitsUsed{};
size_t fillup(byte bits, size_t count) {
const auto freeBits = BITS_PER_BYTE - m_bitsUsed;
if (freeBits == 0) {
return 0;
}
// If the total count is above the free bits available in the current
// byte, then push just a chunk to fill up the current byte
if (freeBits < count) {
return fillup(bits, freeBits);
}
if (m_bitsUsed == 0u) {
m_buffer.data[m_buffer.size] = 0u;
}
// Obviously the total count is less, than the amount of free bits
// available in the current byte, so the following block will simply
// add the bits to the current byte and return
const auto mask = uint32_t(1u << count) - 1u;
m_buffer.data[m_buffer.size] |= byte(bits & mask) << m_bitsUsed;
m_bitsUsed += count;
if (m_bitsUsed == BITS_PER_BYTE) {
m_buffer.size++;
m_bitsUsed = 0u;
}
return count;
}
};
template <size_t S>
BufferFillerImpl<S> BufferFiller(Buffer<S>& buffer) {
return BufferFillerImpl<S>(buffer);
}
@@ -0,0 +1,74 @@
// This file is part of Necroware's GamePort adapter firmware.
// Copyright (C) 2021 Necroware
//
// 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 "AnalogJoystick.h"
#include "Joystick.h"
class CHF16CombatStick : public Joystick {
public:
const Description &getDescription() const override {
// CH F16 Combat Stick from 1995
static const Description description{"CH F16 Combat Stick", 3, 10, 1};
return description;
}
const State &getState() const override {
return m_state;
}
bool init() override {
return true;
}
bool update() override {
const auto decodeHat = [](byte code) -> byte {
// Same as CH Flight Stick Pro. But the F16 stick has 4 positions on the hat only.
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
static constexpr byte table[16] = {0, 0, 0, 7, 0, 0, 0, 5, 0, 0, 0, 3, 0, 0, 0, 1};
return (code < sizeof(table)) ? table[code] : 0u;
};
const auto decodeButtons = [](byte code) -> uint16_t {
// Map to 10 buttons: 5 real ones, one hat with 4 positions. Thus we need uint16 instead of byte
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
static constexpr uint16_t table[16] = {0, 1, 8, 0, 4, 32, 256, 0, 16, 2, 128, 0, 512, 0, 64, 0};
static constexpr auto tableSize = sizeof(table) / sizeof(table[0]);
return (code < tableSize) ? table[code] : 0u;
};
// The 4th axis (index 2) is ignored, because there is a big jitter
// and it reacts on movement of the other axes too. So you can not
// assign the axes to a function in your game.
m_state.axes[0] = m_joystick.getAxis(0);
m_state.axes[1] = m_joystick.getAxis(1);
m_state.axes[2] = m_joystick.getAxis(3); // Throttle
const auto code = m_joystick.getButtons();
m_state.hat = decodeHat(code);
m_state.buttons = decodeButtons(code);
log("Code %d : %d , A2 %d", code, m_state.buttons, m_state.axes[2] );
return true;
}
private:
AnalogJoystick m_joystick;
State m_state;
};
@@ -0,0 +1,68 @@
// This file is part of Necroware's GamePort adapter firmware.
// Copyright (C) 2021 Necroware
//
// 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 "AnalogJoystick.h"
#include "Joystick.h"
class CHFlightstickPro : public Joystick {
public:
const Description &getDescription() const override {
static const Description description{"CH FlightStick Pro", 4, 4, 1};
return description;
}
const State &getState() const override {
return m_state;
}
bool init() override {
return true;
}
bool update() override {
// Unfortunately I had no real CHFlighstickPro joystick to test, but
// Sidewinder 3D Pro has an emulation for CHFlighstickPro. So, this
// implementation was made using that emulation and could be wrong.
// CHFlighstickPro seems to be a very interesting joystick. It doesn't
// allow the user to press multiple buttons simultaneously and uses
// combined buttons invocations as hat switch codes instead. So, every
// time a multiple buttons seem to be pressed, means that the user is
// actually using the hat switch.
const auto decode = [](byte code) -> byte {
static const byte table[16] = {0, 0, 0, 7, 0, 6, 0, 5, 0, 4, 0, 3, 0, 2, 0, 1};
return (code < sizeof(table)) ? table[code] : 0u;
};
for (auto i = 0u; i < 4; i++) {
m_state.axes[i] = m_joystick.getAxis(i);
}
const auto code = m_joystick.getButtons();
m_state.hat = decode(code);
m_state.buttons = m_state.hat ? 0u : code;
return true;
}
private:
AnalogJoystick m_joystick;
State m_state;
};
+162
View File
@@ -0,0 +1,162 @@
// This file is part of Necroware's GamePort adapter firmware.
// Copyright (C) 2021 Necroware
//
// 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 <Arduino.h>
/// Digital signal edge types.
enum class Edge { any, falling, rising };
/// Digital pin base constants.
///
/// This implementation was born out of the need for faster digital I/O. The
/// original digitalRead(...) function of Arduino suite needs about 2.7us per
/// call on a 16MHz MCU. Which makes it impossible to poll 5us pulses, not
/// talking about doing with the data something in between. This was a hard
/// requirement for this project, so following solution is up to 50% faster
/// and needs about 1.6us per call on the same hardware.
template <int Id>
struct DigitalPin {
using RegType = uint8_t;
const RegType mask;
const RegType port;
DigitalPin()
: mask(digitalPinToBitMask(Id))
, port(digitalPinToPort(Id)) {
}
};
/// Digital Output class.
template <int Id>
class DigitalOutput {
public:
/// Constructor.
DigitalOutput()
: m_output(*portOutputRegister(m_pin.port)) {
*portModeRegister(m_pin.port) |= m_pin.mask;
}
/// Sets output high.
void setHigh() const {
m_output |= m_pin.mask;
}
/// Sets output low.
void setLow() const {
m_output &= ~m_pin.mask;
}
/// Sets output to the given value.
void set(bool value) const {
value ? setHigh() : setLow();
}
/// Toggles the output.
void toggle() const {
m_output ^= m_pin.mask;
}
/// Triggers a pulse of given duration.
/// @param[in] duration is the duration in microseconds
void pulse(uint16_t duration = 0) const {
toggle();
if (duration) {
delayMicroseconds(duration);
}
toggle();
}
private:
const DigitalPin<Id> m_pin;
volatile typename DigitalPin<Id>::RegType &m_output;
};
/// Digital input class.
template <int Id, bool Pullup = true>
class DigitalInput {
public:
/// Constructor.
DigitalInput()
: m_input(*portInputRegister(m_pin.port)) {
*portModeRegister(m_pin.port) &= ~m_pin.mask;
if (Pullup) {
*portOutputRegister(m_pin.port) |= m_pin.mask;
}
}
operator bool() const {
return isHigh();
}
/// Read raw bit data
uint8_t read() const {
return m_input & m_pin.mask;
}
/// Checks if the input is high.
bool isHigh() const {
return read();
}
/// Checks if the input is low
bool isLow() const {
return !read();
}
/// Waits for an edge with given timeout.
/// @param[in] edge is the type of edge to wait for
/// @param[in] timeount is the timeout in microseconds
uint16_t wait(Edge edge, uint16_t timeout) const {
if (edge == Edge::falling) {
return waitImpl(timeout, [](uint8_t a, uint8_t) {return a;});
}
if (edge == Edge::rising) {
return waitImpl(timeout, [](uint8_t, uint8_t b) {return b;});
}
// edge == Edge::rising
return waitImpl(timeout, [](uint8_t a, uint8_t b) {return a|b;});
}
/// Waits for a state with given timeout.
/// @param[in] state is the state to wait for
/// @param[in] timeount is the timeout in microseconds
uint16_t wait(bool state, uint16_t timeout) const {
for (; state != isHigh() && timeout; timeout--)
;
return timeout;
}
private:
DigitalPin<Id> m_pin;
volatile typename DigitalPin<Id>::RegType &m_input;
template <typename T>
uint16_t waitImpl(uint16_t timeout, T compare) const {
auto last = read();
for (; timeout; timeout--) {
const auto next = read();
if (last != next) {
if (compare(last, next)) {
return timeout;
}
last = next;
}
}
return 0u;
}
};
+89
View File
@@ -0,0 +1,89 @@
// This file is part of Necroware's GamePort adapter firmware.
// Copyright (C) 2021 Necroware
//
// 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
// Simple GamePort pins to Arduino pins mapper.
// GamePort pins <1>, <8>, <9> are already connected to "VCC" via the PCB
template <int I>
struct GamePort {
enum { pin = -1 };
};
// Analog: Button #1
// Digital (e.g. SW3D): Clock
template <>
struct GamePort<2> {
enum { pin = 10 };
};
// Analog: Joystick1 X-Axis
// Digital (e.g. SW3D): Trigger
template <>
struct GamePort<3> {
enum { pin = A0 };
};
// Analog: Joystick1 Y-Axis
template <>
struct GamePort<6> {
enum { pin = A7 };
};
// Analog: Button #2
// Digital (e.g. SW3D): Data 0
template <>
struct GamePort<7> {
enum { pin = 8 };
};
// Analog: Button #3
// Digital (e.g. SW3D): Data 1
template <>
struct GamePort<10> {
enum { pin = 16 };
};
// Analog: Joystick2 X-Axis
template <>
struct GamePort<11> {
enum { pin = A1 };
};
// Analog: Midi OUT
template <>
struct GamePort<12> {
enum { pin = 5 };
};
// Analog: Joystick2 Y-Axis
template <>
struct GamePort<13> {
enum { pin = A6 };
};
// Analog: Button #4
// Digital (e.g. SW3D): Data 2
template <>
struct GamePort<14> {
enum { pin = 7 };
};
// Analog: Midi IN
template <>
struct GamePort<15> {
enum { pin = 9 };
};
@@ -0,0 +1,54 @@
// This file is part of Necroware's GamePort adapter firmware.
// Copyright (C) 2021 Necroware
//
// 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 "Joystick.h"
#include "AnalogJoystick.h"
template <size_t Axes, size_t Buttons>
class GenericJoystick : public Joystick {
public:
static_assert(Axes > 0 && Axes <= 4);
bool init() override {
return true;
}
bool update() override {
for (auto i = 0u; i < Axes; i++) {
m_state.axes[i] = m_joystick.getAxis(i);
}
m_state.buttons = m_joystick.getButtons();
return true;
}
const State& getState() const override {
return m_state;
}
const Description& getDescription() const override {
static const Description description {
"Generic Joystick", Axes, Buttons, 0
};
return description;
}
private:
AnalogJoystick m_joystick;
State m_state;
};
+120
View File
@@ -0,0 +1,120 @@
// This file is part of Necroware's GamePort adapter firmware.
// Copyright (C) 2021 Necroware
//
// 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 "DigitalPin.h"
#include "Joystick.h"
/// Class to communicate with Gravis joysticks using GrIP.
/// @remark This is a green field implementation, but it was heavily
/// inspired by Linux Gravis/Kensington GrIP driver
/// implementation. See
class GrIP : public Joystick {
/// https://github.com/torvalds/linux/blob/master/drivers/input/joystick/grip.c
public:
/// Resets the joystick and tries to detect the model.
bool init() override {
while (!readPacket())
;
return true;
}
/// Reads the joystick state.
/// @returns the state of axis, buttons etc.
/// @remark if reading the state fails, the last known state is
/// returned and the joystick reset is executed.
bool update() override {
const auto packet = readPacket();
if (packet == 0) {
return false;
}
const auto getBit = [&](uint8_t pos) { return uint8_t(packet >> pos) & 1; };
m_state.axes[0] = map(1 + getBit(15) - getBit(16), 0, 2, 0, 1023);
m_state.axes[1] = map(1 + getBit(13) - getBit(12), 0, 2, 0, 1023);
m_state.buttons = getBit(8);
m_state.buttons |= getBit(3) << 1;
m_state.buttons |= getBit(7) << 2;
m_state.buttons |= getBit(6) << 3;
m_state.buttons |= getBit(10) << 4;
m_state.buttons |= getBit(11) << 5;
m_state.buttons |= getBit(5) << 6;
m_state.buttons |= getBit(2) << 7;
m_state.buttons |= getBit(0) << 8;
m_state.buttons |= getBit(1) << 9;
return true;
}
const State &getState() const override {
return m_state;
}
const Description &getDescription() const override {
static Description desc{"Gravis GamePad Pro", 2, 10, 0};
return desc;
}
private:
/// Supported Gravis model types.
///
/// @remark currently only GamePad Pro is supported and
// this enum is not used yet
enum class Model {
/// Unknown model
GRIP_UNKNOWN,
/// GamePad Pro
GRIP_GAMEPAD_PRO,
};
DigitalInput<GamePort<2>::pin, true> m_clock;
DigitalInput<GamePort<7>::pin, true> m_data;
State m_state;
/// Read bits packet from the joystick.
uint32_t readPacket() const {
// Gravis GamePad Pro sends 24 bits long packages of data all the
// time. Every package starts with a binary tag sequence 011111.
static const auto length = 24u;
uint32_t result = 0u;
// read a package of 24 bits
for (auto i = 0u; i < length; i++) {
if (!m_clock.wait(Edge::falling, 100)) {
return 0u;
}
result |= uint32_t(m_data.isHigh()) << i;
}
// alighn the bits to have the binary tag in front. This code
// was taken almost unchanged from the linux kernel.
for (auto i = 0u; i < length; i++) {
result = (result >> 1) | (result & 1) << (length - 1u);
if ((result & 0xfe4210) == 0x7c0000) {
return result;
}
}
return 0u;
}
};
+165
View File
@@ -0,0 +1,165 @@
// This file is part of Necroware's GamePort adapter firmware.
// Copyright (C) 2021 Necroware
//
// 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>
class HidDevice : public PluggableUSBModule {
public:
explicit HidDevice()
: PluggableUSBModule(1, 1, epType) {
PluggableUSB().plug(this);
}
void AppendDescriptor(HIDSubDescriptor *node) {
if (rootNode == nullptr) {
rootNode = node;
} else {
auto current = rootNode;
while (current->next) {
current = current->next;
}
current->next = node;
}
descriptorSize += node->length;
}
int SendReport(uint8_t id, const void *data, int len) const {
const auto ret = USB_Send(pluggedEndpoint, &id, 1);
if (ret < 0) {
return ret;
}
const auto ret2 = USB_Send(pluggedEndpoint | TRANSFER_RELEASE, data, len);
if (ret2 < 0) {
return ret2;
}
return ret + ret2;
}
protected:
int getInterface(uint8_t *interfaceCount) override {
*interfaceCount += 1; // uses 1
HIDDescriptor hidInterface {
D_INTERFACE(pluggedInterface, 1, USB_DEVICE_CLASS_HUMAN_INTERFACE, HID_SUBCLASS_NONE, HID_PROTOCOL_NONE),
D_HIDREPORT(descriptorSize),
D_ENDPOINT(USB_ENDPOINT_IN(pluggedEndpoint), USB_ENDPOINT_TYPE_INTERRUPT, USB_EP_SIZE, 0x01)
};
return USB_SendControl(0, &hidInterface, sizeof(hidInterface));
}
int getDescriptor(USBSetup &setup) override {
// 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 contains the interface number
if (setup.wIndex != pluggedInterface) {
return 0;
}
int total = 0;
for (auto node = rootNode; node; node = node->next) {
const auto res = USB_SendControl(0, node->data, node->length);
if (res < 0) {
return -1;
}
total += res;
}
// 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 total;
}
uint8_t getShortName(char *name) override {
name[0] = 'H';
name[1] = 'I';
name[2] = 'D';
name[3] = 'A' + (descriptorSize & 0x0F);
name[4] = 'A' + ((descriptorSize >> 4) & 0x0F);
return 5;
}
bool setup(USBSetup &setup) override {
if (pluggedInterface != setup.wIndex) {
return false;
}
const auto request = setup.bRequest;
const auto 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 (request == HID_GET_IDLE) {
// TODO: Send8(idle);
}
}
if (requestType == REQUEST_HOSTTODEVICE_CLASS_INTERFACE) {
if (request == HID_SET_PROTOCOL) {
// The USB Host tells us if we are in boot or report mode.
// This only works with a real boot compatible device.
protocol = setup.wValueL;
return true;
}
if (request == HID_SET_IDLE) {
idle = setup.wValueL;
return true;
}
if (request == HID_SET_REPORT) {
// uint8_t reportID = setup.wValueL;
// uint16_t length = setup.wLength;
// uint8_t data[length];
// Make sure to not read more data than USB_EP_SIZE.
// You can read multiple times through a loop.
// The first byte (may!) contain the reportID on a multreport.
// USB_RecvControl(data, length);
}
}
return false;
}
private:
uint8_t epType[1]{EP_TYPE_INTERRUPT_IN};
HIDSubDescriptor *rootNode{nullptr};
uint16_t descriptorSize{0};
uint8_t protocol{HID_REPORT_PROTOCOL};
uint8_t idle{1};
};
+165
View File
@@ -0,0 +1,165 @@
// This file is part of Necroware's GamePort adapter firmware.
// Copyright (C) 2021 Necroware
//
// 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 "Buffer.h"
#include "HidDevice.h"
#include "Joystick.h"
#include "Utilities.h"
#include <Arduino.h>
class HidJoystick {
public:
bool init(Joystick *joystick) {
if (!joystick || !joystick->init()) {
return false;
}
m_joystick = joystick;
m_hidDescription = createDescription(*m_joystick);
m_subDescriptor = new HIDSubDescriptor{m_hidDescription.data, m_hidDescription.size};
m_hidDevice.AppendDescriptor(m_subDescriptor);
log("Detected device: %s", joystick->getDescription().name);
return true;
}
bool update() {
if (!m_joystick || !m_joystick->update()) {
return false;
}
const auto packet = createPacket(*m_joystick);
m_hidDevice.SendReport(DEVICE_ID, packet.data, packet.size);
return true;
}
private:
using BufferType = Buffer<255>;
static const uint8_t DEVICE_ID{3};
static BufferType createDescription(const Joystick &joystick) {
enum class ID : uint8_t {
application = 0x01,
button = 0x09,
collection = 0xa1,
end_collection = 0xc0,
generic_desktop = 0x01,
hat_switch = 0x39,
input = 0x81,
input_const = 0x03,
input_data = 0x02,
joystick = 0x04,
logical_max = 0x26,
logical_min = 0x15,
report_count = 0x95,
report_id = 0x85,
report_size = 0x75,
simulation_controls = 0x02,
throttle = 0xbb,
usage = 0x09,
usage_max = 0x29,
usage_min = 0x19,
usage_page = 0x05,
};
const auto &desc = joystick.getDescription();
BufferType buffer;
auto filler = BufferFiller(buffer);
auto pushData = [&filler](uint8_t size, uint8_t count) {
filler.push(ID::report_size).push(size);
filler.push(ID::report_count).push(count);
filler.push(ID::input).push(ID::input_data);
const auto padding = (size * count) % 8u;
if (padding) {
filler.push(ID::report_size).push<uint8_t>(8u - padding);
filler.push(ID::report_count).push<uint8_t>(1);
filler.push(ID::input).push(ID::input_const);
}
};
filler.push(ID::usage_page).push(ID::generic_desktop);
filler.push(ID::usage).push(ID::joystick);
filler.push(ID::collection).push(ID::application);
filler.push(ID::report_id).push<uint8_t>(DEVICE_ID);
// Push axes
if (desc.numAxes > 0) {
filler.push(ID::usage_page).push(ID::generic_desktop);
for (auto i = 0u; i < desc.numAxes; i++) {
static constexpr uint8_t x_axis = 0x30;
filler.push(ID::usage).push<uint8_t>(x_axis + i);
}
filler.push(ID::logical_min).push<uint8_t>(0);
filler.push(ID::logical_max).push<uint16_t>(1023);
pushData(10, desc.numAxes);
}
// Push hat
if (desc.hasHat) {
filler.push(ID::usage).push(ID::hat_switch);
filler.push(ID::logical_min).push<uint8_t>(1);
filler.push(ID::logical_max).push<uint16_t>(8);
pushData(4, 1);
}
// Push buttons
if (desc.numButtons > 0) {
filler.push(ID::usage_page).push(ID::button);
filler.push(ID::usage_min).push<uint8_t>(1);
filler.push(ID::usage_max).push<uint8_t>(desc.numButtons);
filler.push(ID::logical_min).push<uint8_t>(0);
filler.push(ID::logical_max).push<uint16_t>(1);
pushData(1, desc.numButtons);
}
filler.push(ID::end_collection);
return buffer;
}
static BufferType createPacket(const Joystick &joystick) {
const auto &state = joystick.getState();
const auto &description = joystick.getDescription();
BufferType buffer;
auto filler = BufferFiller(buffer);
for (auto i = 0u; i < description.numAxes; i++) {
filler.push(state.axes[i], 10);
}
filler.align();
if (description.hasHat) {
filler.push(state.hat, 4);
filler.align();
}
if (description.numButtons) {
filler.push(state.buttons, description.numButtons);
filler.align();
}
return buffer;
}
Joystick *m_joystick{};
BufferType m_hidDescription{};
HIDSubDescriptor *m_subDescriptor{};
HidDevice m_hidDevice;
};
+94
View File
@@ -0,0 +1,94 @@
// This file is part of Necroware's GamePort adapter firmware.
// Copyright (C) 2021 Necroware
//
// 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 <Arduino.h>
/// Base class for all joysticks.
class Joystick {
public:
static const auto MAX_AXES{16u};
/// Device description.
///
/// This structure is used to generate the HID description
/// and USB data packages
struct Description {
/// Human readable name.
const char* name;
/// Number of supported axes.
uint8_t numAxes;
/// Number of supported buttons.
uint8_t numButtons;
/// Has HAT.
bool hasHat;
};
/// Joystick state.
struct State {
/// Axes.
///
/// To keep it simple all axes are unified to 10 bits
/// resolution. Every joystick has to map it's values
/// to the values 0..1023 independent from the internal
/// resolution of the device.
uint16_t axes[MAX_AXES]{};
/// Hats.
///
/// HAT is represented through values 0..8 1 is up, 2 is
/// 45° clockwise, 3 is right etc. Center is represented
/// with zero.
uint8_t hat{};
/// Buttons.
///
/// Every bit is a button. 1 is pressed, 0 is released
uint16_t buttons{};
};
/// Initialize joystick.
///
/// @returns True on successful initialization
virtual bool init() = 0;
/// Update state.
///
/// This function is called every time the joystick
/// state has to be updated.
///
/// @returns True on successful initialization
virtual bool update() = 0;
/// Gets the State of the Joystick.
virtual const State &getState() const = 0;
/// Gets the Description of the Joystick.
virtual const Description &getDescription() const = 0;
Joystick() = default;
virtual ~Joystick() = default;
Joystick(const Joystick &) = delete;
Joystick(Joystick &&) = delete;
Joystick &operator=(const Joystick &) = delete;
Joystick &operator=(Joystick &&) = delete;
};
+357
View File
@@ -0,0 +1,357 @@
// This file is part of Necroware's GamePort adapter firmware.
// Copyright (C) 2021 Necroware
//
// 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 "DigitalPin.h"
#include "GamePort.h"
#include "Joystick.h"
#include "Utilities.h"
class Logitech : public Joystick {
public:
bool init() override {
enableDigitalMode();
if (!readMetaData()) {
return false;
}
// Create joystick description
m_description.name = m_metaData.deviceName;
m_description.numAxes = min(Joystick::MAX_AXES,
m_metaData.num10bitAxes +
m_metaData.num8bitAxes +
m_metaData.numSecondaryHats * 2); // Each hat is mapped to two axes
m_description.numButtons = m_metaData.numPrimaryButtons + m_metaData.numSecondaryButtons;
m_description.hasHat = m_metaData.hasHat;
// If the device is a Logitech ThunderPad Digital, manually redefine the gamepad layout to 2 axes and 8 buttons
if(m_metaData.deviceID == DEVICE_THUNDERPAD_DIGITAL){
m_description.numAxes = 2;
m_description.numButtons = 8;
m_description.hasHat = 0;
}
// If the device is a Logitech WingMan Gamepad, manually redefine the gamepad layout to 2 axes and 11 buttons
else if(m_metaData.deviceID == DEVICE_WINGMAN_GAMEPAD){
m_description.numAxes = 2;
m_description.numButtons = 11;
m_description.hasHat = 0;
}
// Initialize axes centers
uint8_t axis = 0u;
for (auto i = 0u; i < m_metaData.num10bitAxes; i++, axis++) {
m_limits[axis] = { 512 - 256, 512 + 256 };
}
for (auto i = 0u; i < m_metaData.num8bitAxes; i++, axis++) {
m_limits[axis] = { 128 - 64, 128 + 64 };
}
return true;
}
bool update() override {
// === Status packet format ===
//
// Offset Bits Description
// --------------------------------------------------------
// 0 4 Low nibble of the Device ID
// 4 4 High nibble of the Device ID
// 8 10*N 10bit axes (N is number of axes)
// ? 8*N 8bit axes (N is number of axes)
// ? 1*N Buttons (N is number of buttons)
// ? R*N Hats (R is resolution, N is number of hats)
// ? 1*N Secondary buttons (N is number of buttons)
// Cyberman 2 seems not to work properly if the packets are
// read too fast. Following delay will ensure, that after the
// last read at least 5ms passed to ensure, that the joystick
// cooled down again.
delayMicroseconds(5000);
const auto packet = readPacket();
if (packet.size != m_metaData.packageSize) {
return false;
}
const auto packetID = getBits(packet, 0, 4) | getBits(packet, 4, 4) << 4;
if (packetID != m_metaData.deviceID) {
return false;
}
State state;
uint16_t offset = 8u;
uint8_t axis = 0u;
for (auto i = 0u; i < m_metaData.num10bitAxes; i++, axis++) {
state.axes[axis] = mapAxisValue(axis, getBits(packet, offset, 10));
offset += 10;
}
for (auto i = 0u; i < m_metaData.num8bitAxes; i++, axis++) {
state.axes[axis] = mapAxisValue(axis, getBits(packet, offset, 8));
offset += 8;
}
uint16_t button = 0u;
for (auto i = 0u; i < m_metaData.numPrimaryButtons; i++) {
state.buttons |= getBits(packet, offset++, 1) << button++;
}
if (m_metaData.hasHat) {
const auto hatResolution = getHatResolution();
state.hat = mapHatValue(getBits(packet, offset, hatResolution));
offset += hatResolution;
// Secondary hats are all shown as dual axes
for (auto i = 0u; i < m_metaData.numSecondaryHats; i++, axis += 2) {
const auto value = mapHatValue(getBits(packet, offset, hatResolution));
offset += hatResolution;
static constexpr uint16_t dx[] = { 511, 511, 1023, 1023, 1023, 511, 0, 0, 0 };
static constexpr uint16_t dy[] = { 511, 0, 0, 511, 1023, 1023, 1023, 511, 0 };
state.axes[axis + 0] = dx[value];
state.axes[axis + 1] = dy[value];
}
}
for (auto i = 0u; i < m_metaData.numSecondaryButtons; i++) {
state.buttons |= getBits(packet, offset++, 1) << button++;
}
// If the device is a Logitech ThunderPad Digital, manually remap up, down, left and right buttons to X and Y axes
if(m_metaData.deviceID == DEVICE_THUNDERPAD_DIGITAL){
const auto value = getBits(packet, 12, 4);
static constexpr uint16_t dx[] = { 511, 0, 511, 0, 1023, 511, 1023, 511, 511, 0, 511, 0, 1023, 511, 1023, 511 };
static constexpr uint16_t dy[] = { 511, 511, 1023, 1023, 511, 511, 1023, 1023, 0, 0, 511, 511, 0, 0, 511, 511 };
state.axes[0] = dx[value];
state.axes[1] = dy[value];
state.buttons &= 0xFF0F;
state.buttons |= (state.buttons & 0x0F00) >> 4;
}
// If the device is a Logitech WingMan Gamepad, manually remap up, down, left and right buttons to X and Y axes
else if(m_metaData.deviceID == DEVICE_WINGMAN_GAMEPAD){
const auto value = getBits(packet, 8, 4);
static constexpr uint16_t dx[] = { 511, 0, 511, 0, 1023, 511, 1023, 511, 511, 0, 511, 0, 1023, 511, 1023, 511 };
static constexpr uint16_t dy[] = { 511, 511, 1023, 1023, 511, 511, 1023, 1023, 0, 0, 511, 511, 0, 0, 511, 511 };
state.axes[0] = dx[value];
state.axes[1] = dy[value];
state.buttons >>= 4;
}
m_state = state;
return true;
}
const State &getState() const override {
return m_state;
}
const Description &getDescription() const override {
return m_description;
}
private:
struct MetaData {
char deviceName[32]{};
uint8_t deviceID{};
uint8_t packageSize{};
uint8_t num8bitAxes{};
uint8_t num10bitAxes{};
uint8_t numPrimaryButtons{};
uint8_t numSecondaryButtons{};
uint8_t numSecondaryHats{};
uint8_t hasHat{};
uint8_t numHatDirections{};
};
struct Limits {
uint16_t min, max;
};
// Logitech Device ID constants, taken from the Linux Kernel ADI driver
enum LogitechDevices{
DEVICE_WINGMAN_EXTREME_DIGITAL = 0x00,
DEVICE_THUNDERPAD_DIGITAL = 0x01,
DEVICE_SIDECAR = 0x02,
DEVICE_CYBERMAN2 = 0x03,
DEVICE_WINGMAN_INTERCEPTOR = 0x04,
DEVICE_WINGMAN_FORMULA = 0x05,
DEVICE_WINGMAN_GAMEPAD = 0x06,
DEVICE_WINGMAN_EXTREME_DIGITAL_3D = 0x07,
DEVICE_WINGMAN_GAMEPAD_EXTREME = 0x08,
DEVICE_WINGMAN_GAMEPAD_USB = 0x09
};
uint16_t mapAxisValue(uint8_t axis, uint16_t value) {
if (value < m_limits[axis].min) {
m_limits[axis].min = value;
} else if (value > m_limits[axis].max) {
m_limits[axis].max = value;
}
return map(value, m_limits[axis].min, m_limits[axis].max, 0, 1023);
}
uint8_t mapHatValue(uint16_t value) const {
return map(value, 0, m_metaData.numHatDirections, 0, 8);
}
uint8_t getHatResolution() const {
uint8_t result = 0u;
for (auto value = m_metaData.numHatDirections; value; value >>= 1) {
result++;
}
return result;
}
/// Internal bit structure which is filled by reading from the joystick.
using Packet = Buffer<255>;
static uint16_t getBits(const Packet& packet, uint8_t offset, uint8_t count) {
uint16_t result = 0u;
if (offset < packet.size && count <= (packet.size - offset)) {
for (auto i = 0u; i < count; i++) {
result = (result << 1) | packet.data[offset + i];
}
}
return result;
}
DigitalOutput<GamePort<3>::pin> m_trigger;
DigitalInput<GamePort<2>::pin, true> m_data0;
DigitalInput<GamePort<7>::pin, true> m_data1;
MetaData m_metaData;
Description m_description;
State m_state;
Limits m_limits[Joystick::MAX_AXES];
void enableDigitalMode() const {
static constexpr uint16_t seq[] = {4, 2, 3, 10, 6, 11, 7, 9, 11, 0};
// Some devices, as the Logitech ThunderPad Digital, require some time for its
// microcontroller to initialize; otherwise the enableDigitalMode command is skipped
// and the device stays in analog mode. Don't use values higher than 100ms, they could
// interfere with the USB initialization
delay(100);
for (auto i = 0u; seq[i]; i++) {
m_trigger.pulse(20u);
delay(seq[i]);
}
}
byte readData() const {
const auto b0 = m_data0.read();
const auto b1 = m_data1.read();
return bool(b0) | bool(b1) << 1;
}
Packet readPacket() const {
static constexpr auto TIMEOUT = 32u;
auto timeout = TIMEOUT;
auto first = true;
Packet packet;
const InterruptStopper noirq;
auto last = readData();
m_trigger.setHigh();
while (timeout-- && packet.size < Packet::MAX_SIZE) {
const auto next = readData();
const auto edge = last ^ next;
if (edge) {
if (first) {
first = false;
} else {
// Normally both data bits should never flip simultaneously.
// We should get either 10, when data1 has flipped, or 01,
// when data0 has flipped. So if we just shift the edge to
// the right once, we will get the needed 1 or 0 bit value.
packet.data[packet.size++] = edge >> 1;
}
last = next;
timeout = TIMEOUT;
}
}
m_trigger.setLow();
return packet;
}
bool readMetaData() {
// === Metadata packet format ===
//
// Offset Bits Description
// --------------------------------------------------------
// 0 10 Metadata package size
// 10 4 Low nibble of the Device ID
// 14 4 High nibble of the Device ID
// 18 4 Feature flags
// 22 10 Status package size
// 32 4 Number of axes (0..15)
// 36 6 Number of buttons (0..63)
// 42 6 Number of hat directions (0..63)
// 48 6 Number of secondary buttons (0..63)
// 54 4 Number of secondary hats (0..15)
// 58 4 Number of 8-bit axes (0..15)
// 62 4 Length of the cname in bytes (0..15)
// 66 8*N Characters of the cname (N is the length)
// === Flags ===
//
// Bit Description
// ---------------------
// 1 Reserved
// 2 Reserved
// 3 Has Hat
// 4 Has 10-bit axes
const auto packet = readPacket();
const auto metaSize = getBits(packet, 0, 10);
if (metaSize != packet.size) {
log("Meta data package size mismatch, expected %d but got %d", packet.size, metaSize);
return false;
}
m_metaData.deviceID = getBits(packet, 10, 4) | getBits(packet, 14, 4) << 4;
const auto flags = getBits(packet, 18, 4);
m_metaData.hasHat = flags & 0x4;
m_metaData.packageSize = getBits(packet, 22, 10);
const auto numTotalAxes = getBits(packet, 32, 4);
m_metaData.numPrimaryButtons = getBits(packet, 36, 6);
m_metaData.numHatDirections = getBits(packet, 42, 6);
m_metaData.numSecondaryButtons = getBits(packet, 48, 6);
m_metaData.numSecondaryHats = getBits(packet, 54, 4);
const auto num8bitAxes = getBits(packet, 58, 4);
if (flags & 0x8) {
m_metaData.num10bitAxes = numTotalAxes - num8bitAxes;
m_metaData.num8bitAxes = num8bitAxes;
} else {
m_metaData.num8bitAxes = numTotalAxes;
m_metaData.num10bitAxes = 0u;
}
const auto cnameLength = getBits(packet, 62, 4);
m_metaData.deviceName[cnameLength] = 0;
for (auto i = 0u; i < cnameLength; i++) {
m_metaData.deviceName[i] = getBits(packet, 66 + 8 * i, 8);
}
return true;
}
};
+514
View File
@@ -0,0 +1,514 @@
// This file is part of Necroware's GamePort adapter firmware.
// Copyright (C) 2021 Necroware
//
// 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 "Buffer.h"
#include "DigitalPin.h"
#include "Joystick.h"
#include "Utilities.h"
/// Class to for communication with Sidewinder joysticks.
/// @remark This is a green field implementation, but it was heavily
/// inspired by Linux Sidewinder driver implementation. See
/// https://github.com/torvalds/linux/blob/master/drivers/input/joystick/sidewinder.c
class Sidewinder : public Joystick {
public:
/// Resets the joystick and tries to detect the model.
bool init() override {
log("Sidewinder init...");
m_errors = 0;
m_model = guessModel(readPacket());
while (m_model == Model::SW_UNKNOWN) {
// No data. 3d Pro analog mode?
enableDigitalMode();
m_model = guessModel(readPacket());
}
log("Detected model %d", m_model);
return true;
}
bool update() override {
const auto packet = readPacket();
State state;
if (decode(packet, state)) {
m_state = state;
m_errors = 0;
return true;
}
m_errors++;
log("Packet decoding failed %d time(s)", m_errors);
if (m_errors > 5) {
return init();
}
return false;
}
const State &getState() const override {
return m_state;
}
const Description &getDescription() const override;
private:
/// Supported Sidewinder model types.
enum class Model {
/// Unknown model.
SW_UNKNOWN,
/// Sidewinder GamePad
SW_GAMEPAD,
/// Sidewinder 3D Pro
SW_3D_PRO,
/// Sidewinder Precision Pro
SW_PRECISION_PRO,
/// Sidewinder Force Feedback Pro
SW_FORCE_FEEDBACK_PRO,
/// Sidewinder Force Feedback Wheel
SW_FORCE_FEEDBACK_WHEEL
};
/// Internal bit structure which is filled by reading from the joystick.
using Packet = Buffer<128u>;
/// Model specific status decoder function.
template <Model M>
struct Decoder {
static const Description &getDescription();
static bool decode(const Packet &packet, State &state);
};
/// Guesses joystick model from the size of the packet.
Model guessModel(const Packet &packet) const {
log("Guessing model by packet size of %d", packet.size);
switch (packet.size) {
case 15:
return Model::SW_GAMEPAD;
case 16: // 3bit mode
case 48: { // 1bit mode
const auto id = readID(packet.size);
log("Data packet size is ambiguous. Guessing by ID %d", id);
if (id == 14) {
return Model::SW_FORCE_FEEDBACK_PRO;
}
return Model::SW_PRECISION_PRO;
}
case 11: // 3bit mode
case 33: // 1bit mode
return Model::SW_FORCE_FEEDBACK_WHEEL;
case 64:
return Model::SW_3D_PRO;
default:
return Model::SW_UNKNOWN;
}
}
void cooldown() const {
m_trigger.setLow();
delay(3);
}
void trigger() const {
m_trigger.pulse(20);
}
DigitalInput<GamePort<2>::pin, true> m_clock;
DigitalInput<GamePort<7>::pin, true> m_data0;
DigitalInput<GamePort<10>::pin, true> m_data1;
DigitalInput<GamePort<14>::pin, true> m_data2;
DigitalOutput<GamePort<3>::pin> m_trigger;
Model m_model{Model::SW_UNKNOWN};
State m_state{};
uint8_t m_errors{};
/// Enables digital mode for 3D Pro.
//
/// The 3D Pro can work as legacy analog joystick or in digital mode.
/// This mode has to be activated explicitly. In this function timing
/// is very important. See Patent: US#5628686 (page 19) for details.
void enableDigitalMode() const {
static const uint16_t magic = 150;
static const uint16_t seq[] = {magic, magic + 725, magic + 300, magic, 0};
log("Trying to enable digital mode");
cooldown();
const InterruptStopper interruptStopper;
for (auto i = 0u; seq[i]; i++) {
trigger();
delayMicroseconds(seq[i]);
}
}
/// Read bits packet from the joystick.
///
/// This part is extremely performance and timing critical. Change only, if
/// you know, what you are doing.
Packet readPacket() const {
// Packet instantiation is a very expensive call, which zeros the memory.
// The instantiation should therefore happen outside of the interrupt stopper
// and before triggering the device. Otherwise the clock will come before
// the packet was zeroed/instantiated.
Packet packet;
// We are reading into a byte array instead of an uint64_t, because of two
// reasons. First, bits packets can be larger, than 64 bits. We are actually
// not interested in packets, which are larger than that, but may be in the
// future we'd need to handle them as well. Second, for reading into an
// uint64_t we would need to shift between the clock impulses, which is
// impossible to do in time. Unfortunately this shift is extremely slow on
// an Arduino and it's just faster to write into an array. One bit per byte.
packet.size = readBits(Packet::MAX_SIZE, [this, &packet](uint8_t pos) {
const auto b1 = m_data0.read();
const auto b2 = m_data1.read();
const auto b3 = m_data2.read();
packet.data[pos] = bool(b1) | bool(b2) << 1 | bool(b3) << 2;
});
return packet;
}
uint8_t readID(uint8_t dataPacketSize) const {
const auto rise = dataPacketSize / 2 - 1;
const auto fall = rise + 2;
const auto count = readBits(255u, [this, rise, fall](uint8_t pos) {
if (pos == rise) {
m_trigger.setHigh();
}
else if (pos == fall) {
m_trigger.setLow();
}
});
return count < dataPacketSize ? 0 : count - dataPacketSize;
}
template <typename T>
uint8_t readBits(uint8_t maxCount, T&& extract) const {
static const uint8_t wait_duration = 100;
uint8_t count{};
cooldown();
// WARNING: Here starts the timing critical section
const InterruptStopper interruptStopper;
trigger();
if (m_clock.wait(true, wait_duration)) {
while(count < maxCount && m_clock.wait(Edge::rising, wait_duration)) {
extract(count++);
}
}
return count;
}
/// Decodes bit packet into a state.
bool decode(const Packet &packet, State &state) const;
};
/// Placeholder for Unknown Device
template <>
class Sidewinder::Decoder<Sidewinder::Model::SW_UNKNOWN> {
public:
static const Description &getDescription() {
static const Description desc{"Unknown", 0, 0, 0};
return desc;
}
static bool decode(const Packet &, State &) {
return false;
}
};
/// Bit decoder for Sidewinder GamePad.
template <>
class Sidewinder::Decoder<Sidewinder::Model::SW_GAMEPAD> {
public:
static const Description &getDescription() {
static const Description desc{"MS Sidewinder GamePad", 2, 10, 0};
return desc;
}
static bool decode(const Packet &packet, State &state) {
const auto checksum = [&]() {
byte result = 0u;
for (auto i = 0u; i < packet.size; i++) {
result ^= packet.data[i] & 1;
}
return result;
};
if (packet.size != 15 || checksum() != 0) {
return false;
}
// Bit 0-1: x-axis (10-left, 01-right, 11-middle)
// Bit 2-3: y-axis (01-up, 10-down, 11-middle)
// Bit 4-13: 10 buttons
// Bit 14: checksum
for (auto i = 0u; i < 10; i++) {
state.buttons |= (~packet.data[i + 4] & 1) << i;
}
state.axes[0] = map(1 + packet.data[3] - packet.data[2], 0, 2, 0, 1023);
state.axes[1] = map(1 + packet.data[0] - packet.data[1], 0, 2, 0, 1023);
return true;
}
};
/// Bit decoder for Sidewinder 3D Pro.
template <>
class Sidewinder::Decoder<Sidewinder::Model::SW_3D_PRO> {
public:
static const Description &getDescription() {
static const Description desc{"MS Sidewinder 3D Pro", 4, 8, 1};
return desc;
}
static bool decode(const Packet &packet, State &state) {
const auto value = [&]() {
uint64_t result{0u};
for (auto i = 0u; i < packet.size; i++) {
result |= uint64_t(packet.data[i] & 1) << i;
}
return result;
}();
const auto bits = [&](uint8_t start, uint8_t length) {
const auto mask = (1 << length) - 1;
return (value >> start) & mask;
};
if (packet.size != 64 || !checkSync(value) || checksum(value)) {
return false;
}
// bit 38: button 8 + bit 8-15: buttons 1-7 (low active)
state.buttons = ~(bits(8, 7) | (bits(38, 1) << 7));
// bit 3-5 + bit 16-22: x-axis (value 0-1023)
state.axes[0] = bits(3, 3) << 7 | bits(16, 7);
// bit 0-2 + bit 24-30: y-axis (value 0-1023)
state.axes[1] = bits(0, 3) << 7 | bits(24, 7);
// bit 35-36 + bit 40-46: z-axis (value 0-511)
state.axes[2] = map(bits(35, 2) << 7 | bits(40, 7), 0, 511, 0, 1023);
// bit 32-34 + bit 48-54: throttle-axis (value 0-1023)
state.axes[3] = bits(32, 3) << 7 | bits(48, 7);
// bit 6-7 + bit 60-62 (9 pos, 0 center, 1-8 clockwise)
state.hat = bits(6, 1) << 3 | bits(60, 3);
return true;
}
private:
/// Checks sync bits.
///
/// This code was taken from Linux driver as is.
static bool checkSync(uint64_t value) {
return !((value & 0x8080808080808080ULL) ^ 0x80);
}
/// Calculates checksum.
///
/// This code was taken from Linux driver as is.
static byte checksum(uint64_t value) {
auto result = 0u;
while (value) {
result += value & 0xf;
value >>= 4;
}
return result & 0xf;
}
};
/// Bit decoder for Sidewinder Precision Pro
template <>
class Sidewinder::Decoder<Sidewinder::Model::SW_PRECISION_PRO> {
public:
static const Description &getDescription() {
static const Description desc{"MS Sidewinder Precision Pro", 4, 9, 1};
return desc;
}
static bool decode(const Packet &packet, State &state) {
// The packet can be either in 3bit or in 1bit mode
if (packet.size != 16 && packet.size != 48) {
return false;
}
const auto value = [&packet]() {
uint64_t result{0u};
const auto shift = 48 / packet.size;
const auto mask = (shift == 3) ? 0b111 : 0b1;
for (auto i = 0u; i < packet.size; i++) {
result |= uint64_t(packet.data[i] & mask) << (i * shift);
}
return result;
}();
// TODO shared code with 3D Pro?
const auto bits = [&value](uint8_t start, uint8_t length) {
const auto mask = (1 << length) - 1;
return (value >> start) & mask;
};
// TODO shared code with GP?
const auto parity = [](uint64_t t) {
uint32_t x = t ^ (t >> 32);
x ^= x >> 16;
x ^= x >> 8;
x ^= x >> 4;
x ^= x >> 2;
x ^= x >> 1;
return x & 1;
};
if (!parity(value)) {
return false;
}
state.axes[0] = bits(9, 10);
state.axes[1] = bits(19, 10);
state.axes[2] = map(bits(36, 6), 0, 63, 0, 1023);
state.axes[3] = map(bits(29, 7), 0, 127, 0, 1023);
state.hat = bits(42, 4);
state.buttons = ~bits(0, 9);
return true;
}
};
/// Descriptor for Sidewinder Force Feedback Pro.
/// (The bit decoder is identical to the Precision Pro.)
template <>
class Sidewinder::Decoder<Sidewinder::Model::SW_FORCE_FEEDBACK_PRO> {
public:
static const Description &getDescription() {
static const Description desc{"MS Sidewinder Force Feedback Pro", 4, 9, 1};
return desc;
}
static bool decode(const Packet &packet, State &state) {
// Decode is identical between the Force Feedback Pro and the Precision Pro.
return Decoder<Model::SW_PRECISION_PRO>::decode(packet, state);
}
};
/// Bit decoder for Sidewinder Force Feedback Wheel.
template <>
class Sidewinder::Decoder<Sidewinder::Model::SW_FORCE_FEEDBACK_WHEEL> {
public:
static const Description &getDescription() {
static const Description desc{"MS ForceFeedBack Wheel", 3, 8, 0};
return desc;
}
static bool decode(const Packet &packet, State &state) {
// The packet can be either in 3bit or in 1bit mode
if (packet.size != 11 && packet.size != 33) {
return false;
}
const auto value = [&packet]() {
uint64_t result{0u};
const auto shift = 33 / packet.size;
const auto mask = (shift == 3) ? 0b111 : 0b1;
for (auto i = 0u; i < packet.size; i++) {
result |= uint64_t(packet.data[i] & mask) << (i * shift);
}
return result;
}();
// TODO shared code with 3D Pro?
const auto bits = [&value](uint8_t start, uint8_t length) {
const auto mask = (1 << length) - 1;
return (value >> start) & mask;
};
// TODO shared code with GP?
const auto parity = [](uint64_t t) {
uint32_t x = t ^ (t >> 32);
x ^= x >> 16;
x ^= x >> 8;
x ^= x >> 4;
x ^= x >> 2;
x ^= x >> 1;
return x & 1;
};
if (!parity(value)) {
return false;
}
// bit 0-9: RX
state.axes[0] = bits(0, 10);
// bit 10-16: Rudder
state.axes[1] = map(bits(10, 6), 0, 63, 0, 1023);
// bit 16-21: Throttle
state.axes[2] = map(bits(16, 6), 0, 63, 0, 1023);
// bit 22-29: buttons 1-8
state.buttons = ~bits(22, 8);
return true;
}
};
inline const Joystick::Description &Sidewinder::getDescription() const {
switch (m_model) {
case Model::SW_GAMEPAD:
return Decoder<Model::SW_GAMEPAD>::getDescription();
case Model::SW_3D_PRO:
return Decoder<Model::SW_3D_PRO>::getDescription();
case Model::SW_PRECISION_PRO:
return Decoder<Model::SW_PRECISION_PRO>::getDescription();
case Model::SW_FORCE_FEEDBACK_PRO:
return Decoder<Model::SW_FORCE_FEEDBACK_PRO>::getDescription();
case Model::SW_FORCE_FEEDBACK_WHEEL:
return Decoder<Model::SW_FORCE_FEEDBACK_WHEEL>::getDescription();
default:
return Decoder<Model::SW_UNKNOWN>::getDescription();
}
}
inline bool Sidewinder::decode(const Packet &packet, State &state) const {
switch (m_model) {
case Model::SW_GAMEPAD:
return Decoder<Model::SW_GAMEPAD>::decode(packet, state);
case Model::SW_3D_PRO:
return Decoder<Model::SW_3D_PRO>::decode(packet, state);
case Model::SW_PRECISION_PRO:
return Decoder<Model::SW_PRECISION_PRO>::decode(packet, state);
case Model::SW_FORCE_FEEDBACK_PRO:
return Decoder<Model::SW_FORCE_FEEDBACK_PRO>::decode(packet, state);
case Model::SW_FORCE_FEEDBACK_WHEEL:
return Decoder<Model::SW_FORCE_FEEDBACK_WHEEL>::decode(packet, state);
default:
return Decoder<Model::SW_UNKNOWN>::decode(packet, state);
}
}
+76
View File
@@ -0,0 +1,76 @@
// This file is part of Necroware's GamePort adapter firmware.
// Copyright (C) 2021 Necroware
//
// 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 "AnalogJoystick.h"
#include "Joystick.h"
class ThrustMaster : public Joystick {
public:
const Description &getDescription() const override {
static const Description description{"ThrustMaster", 3, 4, 1};
return description;
}
const State &getState() const override {
return m_state;
}
bool init() override {
return true;
}
bool update() override {
// Unfortunately I had no real ThrustMaster joystick to test, but
// Sidewinder 3D Pro has an emulation for ThrustMaster. So, this
// implementation was made using that emulation and could be wrong.
// However the ThrustMaster seem to have a strange hat switch. It is
// encoded using the 3rd axis with values between 0 and 512, where
// 0-127 is up, 128-255 is right, 256-383 is down and 384-479 is left.
// Everything above 480 considered to be middle.
const auto hat = [](uint16_t value) -> uint8_t {
if (value < 128) {
return 1;
}
if (value < 256) {
return 3;
}
if (value < 384) {
return 5;
}
if (value < 480) {
return 7;
}
return 0;
};
for (auto i = 0u; i < 3; i++) {
m_state.axes[i] = m_joystick.getAxis(i);
}
m_state.hat = hat(m_joystick.getAxis(3));
m_state.buttons = m_joystick.getButtons();
return true;
}
private:
AnalogJoystick m_joystick;
State m_state;
};
+61
View File
@@ -0,0 +1,61 @@
// This file is part of Necroware's GamePort adapter firmware.
// Copyright (C) 2021 Necroware
//
// 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 <Arduino.h>
#include <stdarg.h>
#include <stdio.h>
/// Debug messages on serial port are turned off by default. Comment the following
/// line to enable logging to the serial port.
/// Arduino Micro seems somehow to share the serial port with the USB interface.
/// If the serial port will be activated, the operating system will no longer
/// recognize the USB device!
#define NDEBUG
#ifdef NDEBUG
#define initLog()
#define log(...)
#else
inline void initLog() {
Serial.begin(9600);
while(!Serial);
}
inline void log(const char *fmt, ...) {
va_list args;
va_start(args, fmt);
char buffer[512];
vsnprintf(buffer, sizeof(buffer), fmt, args);
va_end(args);
Serial.println(buffer);
}
#endif // !NDEBUG
/// Interrupt guard (RAII).
///
/// This class is used to deactivate the interrupts in performance
/// critical sections. The interrupt is reactivated as soon as this
/// guard runs out of scope.
struct InterruptStopper {
InterruptStopper() { noInterrupts(); }
~InterruptStopper() { interrupts(); }
InterruptStopper(const InterruptStopper&) = delete;
InterruptStopper(InterruptStopper&&) = delete;
InterruptStopper& operator=(const InterruptStopper&) = delete;
InterruptStopper& operator=(InterruptStopper&&) = delete;
};
@@ -0,0 +1,79 @@
// This file is part of Necroware's GamePort adapter firmware.
// Copyright (C) 2021 Necroware
//
// 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 "DigitalPin.h"
#include "HidJoystick.h"
#include "CHFlightstickPro.h"
#include "CHF16CombatStick.h"
#include "GenericJoystick.h"
#include "GrIP.h"
#include "Logitech.h"
#include "Sidewinder.h"
#include "ThrustMaster.h"
static Joystick *createJoystick() {
const auto sw1 = DigitalInput<14, true>{};
const auto sw2 = DigitalInput<15, true>{};
const auto sw3 = DigitalInput<20, true>{};
const auto sw4 = DigitalInput<21, true>{};
// Give some time to setup the input
delay(1);
const auto sw = !sw4 << 3 | !sw3 << 2 | !sw2 << 1 | !sw1;
switch (sw) {
case 0b0001:
return new GenericJoystick<2,4>;
case 0b0010:
return new GenericJoystick<3,4>;
case 0b0011:
return new GenericJoystick<4,4>;
case 0b0100:
return new CHFlightstickPro;
case 0b0101:
return new ThrustMaster;
case 0b0110:
return new CHF16CombatStick;
case 0b0111:
return new Sidewinder;
case 0b1000:
return new GrIP;
case 0b1001:
return new Logitech;
default:
return new GenericJoystick<2,2>;
}
}
void setup() {
// DEBUG information: Debugging is turned off by default
// Comment the "NDEBUG" line in "Utilities.h" to enable logging to the serial monitor
initLog();
}
void loop() {
static auto hidJoystick = [] {
HidJoystick hidJoystick;
hidJoystick.init(createJoystick());
return hidJoystick;
}();
hidJoystick.update();
}