[ Web Proxy ]
URL:
Viewing: https://raw.githubusercontent.com/dmadison/Sim-Racing-Arduino/master/src/SimRacing.cpp [Back]  [Original]

/*
 *  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 

Web Proxy Viewer  |  New URL  |  Original Page