/*
* Project Sim Racing Library for Arduino
* @author David Madison
* @link github.com/dmadison/Sim-Racing-Arduino
* @license LGPLv3 - Copyright (c) 2022 David Madison
*
* This file is part of the Sim Racing Library for Arduino.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see .
*/
#include "SimRacing.h"
/**
* @file SimRacing.cpp
* @brief Source file for the Sim Racing Library
*/
namespace SimRacing {
#if defined(__AVR_ATmega32U4__) || defined(SIM_RACING_DOXYGEN)
template
LogitechPedals CreateShieldObject() {
// Power (VCC): DE-9 pin 9, bridged to DE-9 pin 6
// Ground (GND): DE-9 pin 1
const PinNum Pin_Gas = A2; // DE-9 pin 2
const PinNum Pin_Brake = A1; // DE-9 pin 3
const PinNum Pin_Clutch = A0; // DE-9 pin 4
const PinNum Pin_Detect = 10; // DE-9 pin 6, requires 10k Ohm pull-down
return LogitechPedals(Pin_Gas, Pin_Brake, Pin_Clutch, Pin_Detect);
}
template
LogitechPedals CreateShieldObject() {
// version 2 of the pedals shield has the same pinout,
// so we can use the v1 function
return CreateShieldObject();
}
template
LogitechShifter CreateShieldObject() {
// Power (VCC): DE-9 pin 9, bridged to DE-9 pin 7
// Ground (GND): DE-9 pin 6
// DE-9 pin 3 (CS) needs to be pulled-up to VCC
const PinNum Pin_X_Wiper = A1; // DE-9 pin 4
const PinNum Pin_Y_Wiper = A0; // DE-9 pin 8
const PinNum Pin_DataOut = 14; // DE-9 pin 2
const PinNum Pin_Detect = A2; // DE-9 pin 7, requires 10k Ohm pull-down
return LogitechShifter(Pin_X_Wiper, Pin_Y_Wiper, Pin_DataOut, Pin_Detect);
}
template
LogitechShifter CreateShieldObject() {
// version 2 of the shifter shield has the same data pinout for
// the Driving Force shifter, so we can use the v1 function
return CreateShieldObject();
}
template
LogitechShifterG27 CreateShieldObject() {
// Power (VCC): DE-9 pin 9, bridged to DE-9 pin 7
// Ground (GND): DE-9 pin 6
const PinNum Pin_X_Wiper = A1; // DE-9 pin 4
const PinNum Pin_Y_Wiper = A0; // DE-9 pin 8
const PinNum Pin_DataOut = 14; // DE-9 pin 2
const PinNum Pin_Latch = 10; // DE-9 pin 3, aka chip select, requires 10k Ohm pull-up
const PinNum Pin_Clock = 15; // DE-9 pin 1, should have 470 Ohm resistor to prevent shorts
const PinNum Pin_LED = 16; // DE-9 pin 5, has a 100-120 Ohm series resistor
const PinNum Pin_Detect = A2; // DE-9 pin 7, requires 10k Ohm pull-down
return LogitechShifterG27(Pin_X_Wiper, Pin_Y_Wiper, Pin_Latch, Pin_Clock, Pin_DataOut, Pin_LED, Pin_Detect);
}
template
LogitechShifterG25 CreateShieldObject() {
// Power (VCC): DE-9 pin 9, bridged to DE-9 pin 1
// Ground (GND): DE-9 pin 6
const PinNum Pin_X_Wiper = A1; // DE-9 pin 4
const PinNum Pin_Y_Wiper = A0; // DE-9 pin 8
const PinNum Pin_DataOut = 14; // DE-9 pin 2
const PinNum Pin_Latch = 10; // DE-9 pin 3, aka chip select, requires 10k Ohm pull-up
const PinNum Pin_Clock = A2; // DE-9 pin 7, should have 470 Ohm resistor to prevent shorts
const PinNum Pin_LED = 16; // DE-9 pin 5, has a 100-120 Ohm series resistor
const PinNum Pin_Detect = 15; // DE-9 pin 1, requires 10k Ohm pull-down
return LogitechShifterG25(Pin_X_Wiper, Pin_Y_Wiper, Pin_Latch, Pin_Clock, Pin_DataOut, Pin_LED, Pin_Detect);
}
#endif // ATmega32U4 for shield functions
/**
* Take a pin number as an input and sanitize it to a known working value
*
* In an ideal world this would check against the available pins on the micro,
* but as far as I know the Arduino API does not have a "valid pin" function.
* Instead, we'll just accept any positive number as a pin and reject any
* negative number as invalid ("Unused").
*
* @param pin the pin number to sanitize
* @returns the pin number, or UnusedPin
*/
static constexpr PinNum sanitizePin(PinNum pin) {
return pin < 0 ? UnusedPin : pin;
}
/**
* Invert an input value so it's at the same relative position
* at the other side of an input range.
*
* @param value the value to invert
* @param min the minimum value of the range
* @param max the maximum value of the range
*
* @return the input value, mapped to the other end of the axis
*/
static constexpr long invertAxis(long value, long min, long max) {
return max - value + min; // flip to other side of the scale
}
/**
* Wraps the existing Arduino "map" function to include range checks, so the
* output is never outside the min/max range.
*
* If inMin/inMax are flipped (max less than min), this will adjust the input value
* so its position is relative to the min/max axis. For example, if the min is
* 0 and the max is 100 with an input value of 5, the input value with be set to
* 95 (5 off of max) before being rescaled to the output.
*
* @param value the value to remap to a new range
* @param inMin the minimum range of the input value
* @param inMax the maximum range of the input value
* @param outMin the minimum range of the output value
* @param outMax the maximum range of the output value
*
* @return the remapped value
*/
static long remap(long value, long inMin, long inMax, long outMin, long outMax) {
// if inverted, swap min/max and adjust position of value
if (inMin > inMax) {
const long temp = inMin;
inMin = inMax;
inMax = temp;
value = invertAxis(value, inMin, inMax);
}
if (value = inMax) return outMax;
return map(value, inMin, inMax, outMin, outMax);
}
/**
* Filters a floating point value to a valid percentile range (0-1)
*
* @param pct the input value
* @return the input value limited to 0-1
*/
static float floatPercent(float pct) {
if (pct < 0.0) pct = 0.0;
else if (pct > 1.0) pct = 1.0;
return pct;
}
/**
* Flushes a Stream of input data until no data is remaining.
*
* This includes a delay() statement so that platforms that require a yield()
* call for the watchdog timer don't freeze up.
*
* @param client the Stream client to flush
*/
static void flushClient(Stream& client) {
while (client.read() != -1) { delay(2); } // 9600 baud = ~1 ms per byte
}
/**
* Waits until new data is avaiable on a given Stream interface.
*
* @param client the Stream client to read from
*/
static void waitClient(Stream& client) {
flushClient(client);
while (client.peek() == -1) { delay(1); } // wait for a new byte (using delay to avoid watchdog)
}
/**
* Read a floating point percentage value from a given Stream interface.
*
* The user can skip setting a floating point value by sending the character
* 'n' when prompted. If 'n' is received the value is left unchanged.
*
* Note that this does *not* handle non-numeric strings. If a non-numeric string
* is sent the parseFloat() function will time out and default to "0.0".
*
* @param value the floating point input, passed by reference
* @param client the Stream client to read from and write messages to
*/
static void readFloat(float& value, Stream& client) {
client.print("(to skip this step and go with the default value of '");
client.print(value);
client.print("', send 'n')");
client.println();
waitClient(client);
if (client.peek() == 'n') return; // skip this step
float input;
while (true) {
client.setTimeout(200);
input = client.parseFloat();
if (input >= 0.0 && input = stablePeriod) {
state = ConnectionState::Connected;
}
}
// if we were previously unplugged and are still low, now we're disconnected
else if (state == ConnectionState::Unplug) {
state = ConnectionState::Disconnected;
}
}
}
DeviceConnection::ConnectionState DeviceConnection::getState() const {
return state;
}
bool DeviceConnection::isConnected() const {
return this->getState() == ConnectionState::Connected;
}
void DeviceConnection::setStablePeriod(unsigned long t) {
stablePeriod = t;
if (state == ConnectionState::Connected) {
const unsigned long now = millis();
// if we were previously considered connected, adjust the timestamps
// accordingly so that we still are
if (now - lastChange < stablePeriod) {
lastChange = now - stablePeriod;
}
}
}
bool DeviceConnection::readPin() const {
if (pin == UnusedPin) return HIGH; // if no pin is set, we're always connected
const bool state = digitalRead(pin);
return inverted ? !state : state;
}
//#########################################################
// AnalogInput #
//#########################################################
AnalogInput::AnalogInput(PinNum pin)
: pin(sanitizePin(pin)), position(AnalogInput::Min), cal({AnalogInput::Min, AnalogInput::Max})
{
if (pin != UnusedPin) {
pinMode(pin, INPUT);
}
}
bool AnalogInput::read() {
bool changed = false;
if (pin != UnusedPin) {
const int previous = this->position;
this->position = analogRead(pin);
// check if value is different for 'changed' flag
if (previous != this->position) {
const int rMin = isInverted() ? getMax() : getMin();
const int rMax = isInverted() ? getMin() : getMax();
if (
// if the previous value was under the minimum range
// and the current value is as well, no change
!(previous < rMin && this->position < rMin) &&
// if the previous value was over the maximum range
// and the current value is as well, no change
!(previous > rMax && this->position > rMax)
)
{
// otherwise, the current value is either within the
// range limits *or* it has changed from one extreme
// to the other. Either way, mark it changed!
changed = true;
}
}
}
return changed;
}
long AnalogInput::getPosition(long rMin, long rMax) const {
// inversion is handled within the remap function
return remap(getPositionRaw(), getMin(), getMax(), rMin, rMax);
}
int AnalogInput::getPositionRaw() const {
return this->position;
}
bool AnalogInput::isInverted() const {
return (this->cal.min > this->cal.max); // inverted if min is greater than max
}
void AnalogInput::setPosition(int newPos) {
this->position = newPos;
}
void AnalogInput::setInverted(bool invert) {
if (isInverted() == invert) return; // inversion already set
// to change inversion, swap max and min of the current calibration
AnalogInput::Calibration inverted = { this->cal.max, this->cal.min };
setCalibration(inverted);
}
void AnalogInput::setCalibration(AnalogInput::Calibration newCal) {
this->cal = newCal;
}
//#########################################################
// Peripheral #
//#########################################################
bool Peripheral::update() {
// if the detector exists, poll for state
if (this->detector) {
this->detector->poll();
}
// get the connected state from the detector
const bool connected = this->isConnected();
// call the derived class update function
return this->updateState(connected);
}
bool Peripheral::isConnected() const {
// if detector exists, return state
if (this->detector) {
return this->detector->isConnected();
}
// otherwise, assume always connected
return true;
}
void Peripheral::setDetectPtr(DeviceConnection* d) {
this->detector = d;
}
void Peripheral::setStablePeriod(unsigned long t) {
// if detector exists, set the stable period
if (this->detector) {
this->detector->setStablePeriod(t);
}
}
//#########################################################
// Pedals #
//#########################################################
Pedals::Pedals(AnalogInput* dataPtr, uint8_t nPedals)
:
pedalData(dataPtr),
NumPedals(nPedals),
changed(false)
{}
void Pedals::begin() {
update(); // set initial pedal position
}
bool Pedals::updateState(bool connected) {
this->changed = false;
// if we're connected, read all pedal positions
if (connected) {
for (int i = 0; i < getNumPedals(); ++i) {
changed |= pedalData[i].read();
}
}
// otherwise, zero all pedals
else {
for (int i = 0; i < getNumPedals(); ++i) {
const int min = pedalData[i].getMin();
const int prev = pedalData[i].getPositionRaw();
if (min != prev) {
pedalData[i].setPosition(min);
changed = true;
}
}
}
return this->changed;
}
long Pedals::getPosition(PedalID pedal, long rMin, long rMax) const {
if (!hasPedal(pedal)) return rMin; // not a pedal
return pedalData[pedal].getPosition(rMin, rMax);
}
int Pedals::getPositionRaw(PedalID pedal) const {
if (!hasPedal(pedal)) return AnalogInput::Min; // not a pedal
return pedalData[pedal].getPositionRaw();
}
bool Pedals::hasPedal(PedalID pedal) const {
return (pedal < getNumPedals());
}
void Pedals::setCalibration(PedalID pedal, AnalogInput::Calibration cal) {
if (!hasPedal(pedal)) return;
pedalData[pedal].setCalibration(cal);
pedalData[pedal].setPosition(pedalData[pedal].getMin()); // reset to min position
}
String Pedals::getPedalName(PedalID pedal) {
String name;
switch (pedal) {
case(PedalID::Gas):
name = F("gas");
break;
case(PedalID::Brake):
name = F("brake");
break;
case(PedalID::Clutch):
name = F("clutch");
break;
default:
name = F("???");
break;
}
return name;
}
void Pedals::serialCalibration(Stream& iface) {
const char* separator = "------------------------------------";
iface.println();
iface.println(F("Sim Racing Library Pedal Calibration"));
iface.println(separator);
iface.println();
// read minimums
iface.println(F("Take your feet off of the pedals so they move to their resting position."));
iface.println(F("Send any character to continue."));
waitClient(iface);
const int MaxPedals = 3; // hard-coded at 3 pedals
AnalogInput::Calibration pedalCal[MaxPedals];
// read minimums
for (int i = 0; (i < getNumPedals()) && (i < MaxPedals); i++) {
pedalData[i].read(); // read position
pedalCal[i].min = pedalData[i].getPositionRaw(); // set min to the recorded position
}
iface.println(F("\nMinimum values for all pedals successfully recorded!\n"));
iface.println(separator);
// read maximums
iface.println(F("\nOne at a time, let's measure the maximum range of each pedal.\n"));
for (int i = 0; (i < getNumPedals()) && (i < MaxPedals); i++) {
iface.print(F("Push the "));
String name = getPedalName(static_cast(i));
name.toLowerCase();
iface.print(name);
iface.print(F(" pedal to the floor. "));
iface.println(F("Send any character to continue."));
waitClient(iface);
pedalData[i].read(); // read position
pedalCal[i].max = pedalData[i].getPositionRaw(); // set max to the recorded position
}
// deadzone options
iface.println(separator);
iface.println();
float DeadzoneMin = 0.01; // by default, 1% (trying to keep things responsive)
float DeadzoneMax = 0.025; // by default, 2.5%
iface.println(F("These settings are optional. Send 'y' to customize. Send any other character to continue with the default values."));
iface.print(F(" * Pedal Travel Deadzone, Start: \t"));
iface.print(DeadzoneMin);
iface.println(F(" (Used to avoid the pedal always being slightly pressed)"));
iface.print(F(" * Pedal Travel Deadzone, End: \t"));
iface.print(DeadzoneMax);
iface.println(F(" (Used to guarantee that the pedal can be fully pressed)"));
iface.println();
waitClient(iface);
if (iface.read() == 'y') {
iface.println(F("Set the pedal travel starting deadzone as a floating point percentage."));
readFloat(DeadzoneMin, iface);
iface.println();
iface.println(F("Set the pedal travel ending deadzone as a floating point percentage."));
readFloat(DeadzoneMax, iface);
iface.println();
}
flushClient(iface);
// calculate deadzone offsets
for (int i = 0; (i < getNumPedals()) && (i < MaxPedals); i++) {
auto &cMin = pedalCal[i].min;
auto &cMax = pedalCal[i].max;
const int range = abs(cMax - cMin);
const int dzMin = DeadzoneMin * (float)range;
const int dzMax = DeadzoneMax * (float)range;
// non-inverted
if (cMax >= cMin) {
cMax -= dzMax; // 'cut' into the range so it limits sooner
cMin += dzMin;
}
// inverted
else {
cMax += dzMax;
cMin -= dzMin;
}
}
// print finished calibration
iface.println(F("Here is your calibration:"));
iface.println(separator);
iface.println();
iface.print(F("pedals.setCalibration("));
for (int i = 0; (i < getNumPedals()) && (i < MaxPedals); i++) {
if(i > 0) iface.print(F(", "));
iface.print('{');
iface.print(pedalCal[i].min);
iface.print(F(", "));
iface.print(pedalCal[i].max);
iface.print('}');
this->setCalibration(static_cast(i), pedalCal[i]); // and set it ourselves, too
}
iface.print(");");
iface.println();
iface.println();
iface.println(separator);
iface.println();
iface.print(F("Paste this line into the setup() function. The "));
iface.print(F("pedals"));
iface.print(F(" will be calibrated with these values on startup."));
iface.println(F("\nCalibration complete! :)\n\n"));
flushClient(iface);
}
TwoPedals::TwoPedals(PinNum gasPin, PinNum brakePin)
: Pedals(pedalData, NumPedals),
pedalData{ AnalogInput(gasPin), AnalogInput(brakePin) }
{}
void TwoPedals::setCalibration(AnalogInput::Calibration gasCal, AnalogInput::Calibration brakeCal) {
this->Pedals::setCalibration(PedalID::Gas, gasCal);
this->Pedals::setCalibration(PedalID::Brake, brakeCal);
}
ThreePedals::ThreePedals(PinNum gasPin, PinNum brakePin, PinNum clutchPin)
: Pedals(pedalData, NumPedals),
pedalData{ AnalogInput(gasPin), AnalogInput(brakePin), AnalogInput(clutchPin) }
{}
void ThreePedals::setCalibration(AnalogInput::Calibration gasCal, AnalogInput::Calibration brakeCal, AnalogInput::Calibration clutchCal) {
this->Pedals::setCalibration(PedalID::Gas, gasCal);
this->Pedals::setCalibration(PedalID::Brake, brakeCal);
this->Pedals::setCalibration(PedalID::Clutch, clutchCal);
}
LogitechPedals::LogitechPedals(PinNum gasPin, PinNum brakePin, PinNum clutchPin, PinNum detectPin)
:
ThreePedals(gasPin, brakePin, clutchPin),
detectObj(detectPin, false) // active high
{
this->setDetectPtr(&this->detectObj);
// taken from calibrating my own pedals. the springs are pretty stiff so while
// this covers the whole travel range, users may want to back it down for casual
// use (esp. for the brake travel)
this->setCalibration({ 904, 48 }, { 944, 286 }, { 881, 59 });
}
LogitechDrivingForceGT_Pedals::LogitechDrivingForceGT_Pedals(PinNum gasPin, PinNum brakePin, PinNum detectPin)
:
TwoPedals(gasPin, brakePin),
detectObj(detectPin, false) // active high
{
this->setDetectPtr(&this->detectObj);
this->setCalibration({ 646, 0 }, { 473, 1023 }); // taken from calibrating my own pedals
}
//#########################################################
// Shifter #
//#########################################################
Shifter::Shifter(Gear min, Gear max)
:
MinGear(min), MaxGear(max)
{
this->currentGear = this->previousGear = 0; // neutral
}
void Shifter::setGear(Gear gear) {
// if gear is out of range, set it to neutral
if (gear < MinGear || gear > MaxGear) {
gear = 0;
}
this->previousGear = this->currentGear;
this->currentGear = gear;
}
char Shifter::getGearChar(int gear) {
char c = '?';
switch (gear) {
case(-1):
c = 'r';
break;
case(0):
c = 'n';
break;
default:
if (gear > 0 && gear 9) {
name = F("???");
break; // out of range
}
name = gear; // set string to current gear
switch (gear) {
case(1):
name += F("st");
break;
case(2):
name += F("nd");
break;
case(3):
name += F("rd");
break;
default:
name += F("th");
break;
}
break;
}
}
return name;
}
String Shifter::getGearString() const {
return getGearString(getGear());
}
/* Static calibration constants
* These values are arbitrary - just what worked well with my own shifter.
*/
const float AnalogShifter::CalEngagementPoint = 0.70;
const float AnalogShifter::CalReleasePoint = 0.50;
const float AnalogShifter::CalEdgeOffset = 0.60;
AnalogShifter::AnalogShifter(
Gear gearMin, Gear gearMax,
PinNum pinX, PinNum pinY, PinNum pinRev
) :
Shifter(gearMin, gearMax),
/* Two axes, X and Y */
analogAxis{ AnalogInput(pinX), AnalogInput(pinY) },
pinReverse(sanitizePin(pinRev)),
reverseState(false)
{}
void AnalogShifter::begin() {
if (this->pinReverse != UnusedPin) {
pinMode(pinReverse, INPUT);
}
update(); // set initial gear position
}
bool AnalogShifter::updateState(bool connected) {
// if not connected, reset our position back to neutral
// and immediately return
if (!connected) {
// set axis values to calibrated neutral
analogAxis[Axis::X].setPosition(calibration.neutralX);
analogAxis[Axis::Y].setPosition(calibration.neutralY);
// set reverse state to unpressed
this->reverseState = false;
// set gear to neutral
this->setGear(0);
// status changed if gear changed
return this->gearChanged();
}
// poll the analog axes for new data
analogAxis[Axis::X].read();
analogAxis[Axis::Y].read();
const int x = analogAxis[Axis::X].getPosition();
const int y = analogAxis[Axis::Y].getPosition();
// poll the reverse button and cache in the class
this->reverseState = this->readReverseButton();
// check previous gears for comparison
const Gear previousGear = this->getGear();
const bool prevOdd = ((previousGear != -1) && (previousGear & 1)); // were we previously in an odd gear
const bool prevEven = (!prevOdd && previousGear != 0); // were we previously in an even gear
Gear newGear = 0;
// If we're below the 'release' thresholds, we must still be in the previous gear
if ((prevOdd && y > calibration.oddRelease) || (prevEven && y < calibration.evenRelease)) {
newGear = previousGear;
}
// If we're *not* below the release thresholds, we may be in a different gear
else {
// Check if we're in even or odd gears (Y axis)
if (y > calibration.oddTrigger) {
newGear = 1; // we're in an odd gear
}
else if (y < calibration.evenTrigger) {
newGear = 2; // we're in an even gear
}
if (newGear != 0) {
// Now check *which* gear we're in, if we're in one (X axis)
if (x > calibration.rightEdge) newGear += 4; // 1-2 + 4 = 5-6
else if (x >= calibration.leftEdge) newGear += 2; // 1-2 + 2 = 3-4
// (note the '>=', because it would normally be a 'setGear(newGear);
return this->gearChanged();
}
long AnalogShifter::getPosition(Axis ax, long min, long max) const {
if (ax != Axis::X && ax != Axis::Y) return min; // not an axis
return analogAxis[ax].getPosition(min, max);
}
int AnalogShifter::getPositionRaw(Axis ax) const {
if (ax != Axis::X && ax != Axis::Y) return AnalogInput::Min; // not an axis
return analogAxis[ax].getPositionRaw();
}
bool AnalogShifter::readReverseButton() {
// if the reverse pin is not set, avoid reading the
// floating input and just return 'false'
if (pinReverse == UnusedPin) {
return false;
}
return digitalRead(pinReverse);
}
bool AnalogShifter::getReverseButton() const {
// return the cached reverse state from updateState(bool)
// do NOT poll the button!
return this->reverseState;
}
void AnalogShifter::setCalibration(
GearPosition neutral,
GearPosition g1, GearPosition g2, GearPosition g3, GearPosition g4, GearPosition g5, GearPosition g6,
float engagePoint, float releasePoint, float edgeOffset) {
// limit percentage thresholds
engagePoint = floatPercent(engagePoint);
releasePoint = floatPercent(releasePoint);
edgeOffset = floatPercent(edgeOffset);
const int xLeft = (g1.x + g2.x) / 2; // find the minimum X position average
const int xRight = (g5.x + g6.x) / 2; // find the maximum X position average
const int yOdd = (g1.y + g3.y + g5.y) / 3; // find the maximum Y position average
const int yEven = (g2.y + g4.y + g6.y) / 3; // find the minimum Y position average
// set X/Y calibration and inversion
analogAxis[Axis::X].setCalibration({ xLeft, xRight });
analogAxis[Axis::Y].setCalibration({ yEven, yOdd });
// save neutral values (raw)
calibration.neutralX = neutral.x;
calibration.neutralY = neutral.y;
// get normalized and inverted neutral values
// this lets us take advantage of the AnalogInput normalization function
// that handles inverted axes and automatic range rescaling, so the rest of
// the calibration options can be in the normalized range
const Axis axes[2] = { Axis::X, Axis::Y };
int* const neutralAxis[2] = { &neutral.x, &neutral.y };
for (int i = 0; i < 2; i++) {
const int previous = analogAxis[axes[i]].getPositionRaw(); // save current value
analogAxis[axes[i]].setPosition(*neutralAxis[i]); // set new value to neutral calibration
*neutralAxis[i] = analogAxis[axes[i]].getPosition(); // get normalized neutral value
analogAxis[axes[i]].setPosition(previous); // reset axis position to previous
}
// calculate the distances between each neutral and the limits of each axis
const int yOddDiff = AnalogInput::Max - neutral.y;
const int yEvenDiff = neutral.y - AnalogInput::Min;
const int leftDiff = neutral.x - AnalogInput::Min;
const int rightDiff = AnalogInput::Max - neutral.x;
// calculate and save the trigger and release points for each level
calibration.oddTrigger = neutral.y + ((float)yOddDiff * engagePoint);
calibration.oddRelease = neutral.y + ((float)yOddDiff * releasePoint);
calibration.evenTrigger = neutral.y - ((float)yEvenDiff * engagePoint);
calibration.evenRelease = neutral.y - ((float)yEvenDiff * releasePoint);
calibration.leftEdge = neutral.x - ((float)leftDiff * edgeOffset);
calibration.rightEdge = neutral.x + ((float)rightDiff * edgeOffset);
#if 0
Serial.print("Odd Trigger: ");
Serial.println(calibration.oddTrigger);
Serial.print("Odd Release: ");
Serial.println(calibration.oddRelease);
Serial.print("Even Trigger: ");
Serial.println(calibration.evenTrigger);
Serial.print("Even Release: ");
Serial.println(calibration.evenRelease);
Serial.print("Left Edge: ");
Serial.println(calibration.leftEdge);
Serial.print("Right Edge: ");
Serial.println(calibration.rightEdge);
Serial.println();
Serial.print("X Min: ");
Serial.println(analogAxis[Axis::X].getMin());
Serial.print("X Max: ");
Serial.println(analogAxis[Axis::X].getMax());
Serial.print("Y Min: ");
Serial.println(analogAxis[Axis::Y].getMin());
Serial.print("Y Max: ");
Serial.println(analogAxis[Axis::Y].getMax());
#endif
}
void AnalogShifter::serialCalibration(Stream& iface) {
if (isConnected() == false) {
iface.print(F("Error! Cannot perform calibration, "));
iface.print(F("shifter"));
iface.println(F(" is not connected."));
return;
}
const char* separator = "------------------------------------";
iface.println();
iface.println(F("Sim Racing Library Shifter Calibration"));
iface.println(separator);
iface.println();
AnalogShifter::GearPosition gears[7]; // neutral, then 1-6
float engagementPoint = CalEngagementPoint;
float releasePoint = CalReleasePoint;
float edgeOffset = CalEdgeOffset;
for (int i = 0; i getGearString(i);
iface.print(F("Please move the gear shifter into "));
iface.print(gearName);
iface.println(F(". Send any character to continue."));
waitClient(iface);
this->update();
gears[i] = {
this->analogAxis[Axis::X].getPositionRaw(),
this->analogAxis[Axis::Y].getPositionRaw()
};
iface.print("Gear '");
iface.print(gearName);
iface.print("' position recorded as { ");
iface.print(gears[i].x);
iface.print(", ");
iface.print(gears[i].y);
iface.println(" }");
iface.println();
}
iface.println(separator);
iface.println();
iface.println(F("These settings are optional. Send 'y' to customize. Send any other character to continue with the default values."));
iface.print(F(" * Gear Engagement Point: \t"));
iface.println(engagementPoint);
iface.print(F(" * Gear Release Point: \t"));
iface.println(releasePoint);
iface.print(F(" * Horizontal Gate Offset:\t"));
iface.println(edgeOffset);
iface.println();
waitClient(iface);
if (iface.read() == 'y') {
iface.println(F("Set the engagement point as a floating point percentage. This is the percentage away from the neutral axis on Y to start engaging gears."));
readFloat(engagementPoint, iface);
iface.println();
iface.println(F("Set the release point as a floating point percentage. This is the percentage away from the neutral axis on Y to go back into neutral. It must be less than the engagement point."));
readFloat(releasePoint, iface);
iface.println();
iface.println(F("Set the gate offset as a floating point percentage. This is the percentage away from the neutral axis on X to select the side gears."));
readFloat(edgeOffset, iface);
iface.println();
}
flushClient(iface);
this->setCalibration(gears[0], gears[1], gears[2], gears[3], gears[4], gears[5], gears[6], engagementPoint, releasePoint, edgeOffset);
iface.println(F("Here is your calibration:"));
iface.println(separator);
iface.println();
iface.print(F("shifter.setCalibration("));
for (int i = 0; i < 7; i++) {
iface.print('{');
iface.print(gears[i].x);
iface.print(", ");
iface.print(gears[i].y);
iface.print('}');
iface.print(", ");
}
iface.print(engagementPoint);
iface.print(", ");
iface.print(releasePoint);
iface.print(", ");
iface.print(edgeOffset);
iface.print(");");
iface.println();
iface.println();
iface.println(separator);
iface.println();
iface.println(F("Paste this line into the setup() function to calibrate on startup."));
iface.println(F("\n\nCalibration complete! :)\n"));
}
LogitechShifter::LogitechShifter(PinNum pinX, PinNum pinY, PinNum pinRev, PinNum detectPin)
:
AnalogShifter(
-1, 6, // includes reverse and gears 1-6
pinX, pinY, pinRev
),
detectObj(detectPin, false) // active high
{
this->setDetectPtr(&this->detectObj);
this->setCalibration({ 490, 440 }, { 253, 799 }, { 262, 86 }, { 460, 826 }, { 470, 76 }, { 664, 841 }, { 677, 77 });
}
LogitechShifterG27::LogitechShifterG27(
PinNum pinX, PinNum pinY,
PinNum pinLatch, PinNum pinClock, PinNum pinData,
PinNum pinLed,
PinNum pinDetect
) :
LogitechShifter(pinX, pinY, UnusedPin, pinDetect),
pinLatch(sanitizePin(pinLatch)), pinClock(sanitizePin(pinClock)), pinData(sanitizePin(pinData)),
pinLed(sanitizePin(pinLed))
{
this->pinModesSet = false;
this->setPowerLED(1); // power LED on by default
this->buttonStates = this->previousButtons = 0x0000; // zero all button data
// using the calibration values from my own G27 shifter
this->setCalibration({ 453, 470 }, { 247, 828 }, { 258, 6 }, { 449, 878 }, { 472, 5 }, { 645, 880 }, { 651, 21 });
}
void LogitechShifterG27::cacheButtons(uint16_t newStates) {
this->previousButtons = this->buttonStates; // save current to previous
this->buttonStates = newStates; // replace current with new value
}
void LogitechShifterG27::setPinModes(bool enabled) {
// check if pins are valid. if one or more pins is unused,
// this isn't going to work and we shouldn't bother setting
// any of the pin states
if (
this->pinData == UnusedPin ||
this->pinLatch == UnusedPin ||
this->pinClock == UnusedPin)
{
return;
}
// set up data pin to read from regardless
pinMode(this->pinData, INPUT);
// enabled = drive the output pins
if (enabled) {
// note: writing the output before setting the
// pin mode so that we don't accidentally drive
// the wrong direction momentarily
// set latch pin as output, HIGH on idle
digitalWrite(this->pinLatch, HIGH);
pinMode(this->pinLatch, OUTPUT);
// set clock pin as output, LOW on idle
digitalWrite(this->pinClock, LOW);
pinMode(this->pinClock, OUTPUT);
// if we have an LED pin, set it to output and write the
// commanded state (inverted, as the LED is active-low)
if (this->pinLed != UnusedPin) {
digitalWrite(this->pinLed, !(this->ledState));
pinMode(this->pinLed, OUTPUT);
}
}
// disabled = leave output pins as high-z
else {
// note: setting the mode before writing the
// output for the same reason; changing in
// high-z mode is safer
// set latch pin as high impedance, with pull-up
pinMode(this->pinLatch, INPUT);
digitalWrite(this->pinLatch, HIGH);
// set clock pin as high impedance, no pull-up
pinMode(this->pinClock, INPUT);
digitalWrite(this->pinClock, LOW);
// if we have an LED pin, set it to input, LOW on idle
if (this->pinLed != UnusedPin) {
pinMode(this->pinLed, INPUT);
digitalWrite(this->pinLed, LOW);
}
}
this->pinModesSet = enabled;
}
void LogitechShifterG27::setPowerLED(bool state) {
this->ledState = state;
}
uint16_t LogitechShifterG27::readShiftRegisters() {
// if the pin outputs are not set, quit (none pressed)
if (!this->pinModesSet) return 0x0000;
uint16_t data = 0x0000;
// pulse shift register latch from high to low to high, 12 us
// (this timing is *completely* arbitrary, but it's nice to have
// *some* delay so that much faster MCUs don't blow through it)
digitalWrite(this->pinLatch, LOW);
delayMicroseconds(12);
digitalWrite(this->pinLatch, HIGH);
delayMicroseconds(12);
// clock is pulsed from LOW to HIGH on every bit,
// and then left to idle low
for (int i = 0; i < 16; ++i) {
digitalWrite(this->pinClock, LOW);
const bool state = digitalRead(this->pinData);
if (state) data |= 1 pinClock, HIGH);
delayMicroseconds(6);
}
digitalWrite(this->pinClock, LOW);
// edge case: two of the bits (0x8000 and 0x2000) are connected only to
// pull-down resistors, and should theoretically never be high. If they,
// and all other bits, *are* high, then we are not reading from a shifter
// that has shift registers. The "Driving Force" (G29/G920/G923) shifter
// has its data output connected to the 'reverse' button through a buffer,
// and will report 'high' if the reverse button is pressed no matter how
// many times the clock is pulsed.
//
// QED: we are connected to a "Driving Force" shifter, and not a G27.
// That's okay! If we set the state of the 'reverse' button and clear
// all others, we can still behave like a G27.
if (data == 0xFFFF) {
data = (1 setPinModes(0);
// call the begin() class of the base, which will also
// poll 'update()' on our behalf
this->AnalogShifter::begin();
}
bool LogitechShifterG27::updateState(bool connected) {
bool changed = false;
// if we're connected, set the pin modes, read the
// shift registers, and cache the data
if (connected) {
if (!this->pinModesSet) {
this->setPinModes(1);
}
if (this->pinLed != UnusedPin) {
digitalWrite(this->pinLed, !(this->ledState)); // active low
}
const uint16_t data = this->readShiftRegisters();
this->cacheButtons(data);
changed |= this->buttonsChanged();
}
// if we're *not* connected, reset the pin modes and
// set no buttons pressed
else {
if (this->pinModesSet) {
this->setPinModes(0);
}
this->cacheButtons(0x0000);
changed |= this->buttonsChanged();
}
// we also need to update the data for the analog shifter
changed |= AnalogShifter::updateState(connected);
return changed;
}
bool LogitechShifterG27::buttonsChanged() const {
return this->buttonStates != this->previousButtons;
}
bool LogitechShifterG27::getButton(Button button) const {
return this->extractButton(button, this->buttonStates);
}
bool LogitechShifterG27::getButtonChanged(Button button) const {
return this->getButton(button) != this->extractButton(button, this->previousButtons);
}
int LogitechShifterG27::getDpadAngle() const {
const Button pads[4] = {
DPAD_UP,
DPAD_RIGHT,
DPAD_DOWN,
DPAD_LEFT,
};
// combine pads to a bitfield (nybble)
uint8_t dpad = 0x00;
for (uint8_t i = 0; i < 4; ++i) {
dpad |= (this->getButton(pads[i]) inSequentialMode()) {
// clear 'changed', because this will falsely report a change
// if we've "shifted" into 2nd/4th in the process of sequential
// shifting
changed = false;
// force neutral gear, ignoring the H-pattern selection
this->setGear(0);
// edge case: if we've not just switched into sequential mode,
// we need to ignore the H-pattern gear change (to 2/4, and then
// set by us to neutral). We can do that, hackily, by setting to
// neutral again to clear the cached gear for comparison.
if (this->sequentialProcess) {
this->setGear(0);
}
// read the raw y axis value, ignoring the H-pattern calibration
const int y = this->getPositionRaw(Axis::Y);
// save the previous state for reference
const int8_t prevState = this->sequentialState;
// if we're neutral, check for up/down shift
if (this->sequentialState == 0) {
if (y seqCalibration.upTrigger) this->sequentialState = 1;
else if (y >= this->seqCalibration.downTrigger) this->sequentialState = -1;
}
// if we're in up-shift mode, check for release
else if ((this->sequentialState == 1) && (y > this->seqCalibration.upRelease)) {
this->sequentialState = 0;
}
// if we're in down-shift mode, check for release
else if ((this->sequentialState == -1) && (y < this->seqCalibration.downRelease)) {
this->sequentialState = 0;
}
// set the 'changed' flag if the sequential state changed
if (prevState != this->sequentialState) {
changed = true;
}
// otherwise, set 'changed' based on the buttons *only*
else {
changed = this->buttonsChanged();
}
// set 'process' flag to handle edge case on subsequent updates
this->sequentialProcess = true;
}
// if we're not connected or if the sequential mode has been disabled,
// clear the sequential flags if they have been set
else {
if (this->sequentialProcess) {
this->sequentialProcess = false; // not in sequential mode
this->sequentialState = 0; // no sequential buttons pressed
changed = true;
}
}
return changed;
}
bool LogitechShifterG25::inSequentialMode() const {
return this->getButton(BUTTON_SEQUENTIAL);
}
bool LogitechShifterG25::getShiftUp() const {
return this->sequentialState == 1;
}
bool LogitechShifterG25::getShiftDown() const {
return this->sequentialState == -1;
}
void LogitechShifterG25::setCalibrationSequential(int neutral, int up, int down, float engagePoint, float releasePoint) {
// limit percentage thresholds
engagePoint = floatPercent(engagePoint);
releasePoint = floatPercent(releasePoint);
// prevent release point from being higher than engage
// (which will prevent the shifter from working at all)
if (releasePoint > engagePoint) {
releasePoint = engagePoint;
}
// if up/down calibration points are reversed, swap them
//
// in the original public release, pushing the shifter was 'shift up'
// and pulling the shifter was 'shift down'
//
// this bug was eventually fixed, so that now pushing the shifter is
// 'shift down' and pulling the shifter is 'shift up'. This matches the
// markings on the shifter itself (or mine, at least), and mirrors the
// behavior of a sequential shift lever in a real rally car.
//
// by swapping the calibration points here, the function maintains
// compatibility with calibration lines written for both versions
if(up > down) {
int temp = up;
up = down; // dogs and cats living together, mass hysteria
down = temp;
}
// calculate ranges
const int upRange = neutral - up;
const int downRange = down - neutral;
// calculate calibration points
this->seqCalibration.upTrigger = neutral - (upRange * engagePoint);
this->seqCalibration.upRelease = neutral - (upRange * releasePoint);
this->seqCalibration.downTrigger = neutral + (downRange * engagePoint);
this->seqCalibration.downRelease = neutral + (downRange * releasePoint);
}
void LogitechShifterG25::serialCalibrationSequential(Stream& iface) {
// err if not connected
if (this->isConnected() == false) {
iface.print(F("Error! Cannot perform calibration, "));
iface.print(F("shifter"));
iface.println(F(" is not connected."));
return;
}
const char* separator = "------------------------------------";
iface.println();
iface.println(F("Sim Racing Library G25 Sequential Shifter Calibration"));
iface.println(separator);
iface.println();
while (this->inSequentialMode() == false) {
iface.print(F("Please press down on the shifter and move the dial counter-clockwise to put the shifter into sequential mode"));
iface.print(F(". Send any character to continue."));
iface.println(F(" Send 'q' to quit."));
iface.println();
waitClient(iface);
this->update();
// quit if user sends 'q'
if (iface.read() == 'q') {
iface.println(F("Quitting sequential calibration! Goodbye