Skip to content

Commit 9d1fb71

Browse files
committed
Add Logitech G25 shifter support
Extends the LogitechShifterG27 class, and adds support for the sequential shifting feature
1 parent f22ad32 commit 9d1fb71

2 files changed

Lines changed: 365 additions & 0 deletions

File tree

src/SimRacing.cpp

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1285,6 +1285,271 @@ bool LogitechShifterG27::readReverseButton() {
12851285
}
12861286

12871287

1288+
/*
1289+
* Static calibration constants
1290+
* These values are arbitrary - just what worked well with my own shifter.
1291+
*/
1292+
const float LogitechShifterG25::CalEngagementPoint = 0.70;
1293+
const float LogitechShifterG25::CalReleasePoint = 0.50;
1294+
1295+
LogitechShifterG25::LogitechShifterG25(
1296+
PinNum pinX, PinNum pinY,
1297+
PinNum pinLatch, PinNum pinClock, PinNum pinData,
1298+
PinNum pinDetect,
1299+
PinNum pinLed
1300+
) :
1301+
LogitechShifterG27(
1302+
pinX, pinY,
1303+
pinLatch, pinClock, pinData,
1304+
pinDetect,
1305+
pinLed
1306+
),
1307+
1308+
sequentialProcess(false), // not in sequential mode
1309+
sequentialState(0) // no sequential buttons pressed
1310+
{
1311+
// using the values from my own shifter
1312+
this->setCalibrationSequential(425, 619, 257, 0.70, 0.50);
1313+
}
1314+
1315+
void LogitechShifterG25::begin() {
1316+
this->sequentialProcess = false; // clear process flag
1317+
this->sequentialState = 0; // clear any pressed buttons
1318+
1319+
this->LogitechShifterG27::begin(); // call base class begin()
1320+
}
1321+
1322+
bool LogitechShifterG25::updateState(bool connected) {
1323+
// call the base class to update the state of the
1324+
// buttons and the H-pattern shifter
1325+
bool changed = this->LogitechShifterG27::updateState(connected);
1326+
1327+
// if we're connected and in sequential mode...
1328+
if (connected && this->inSequentialMode()) {
1329+
1330+
// clear 'changed', because this will falsely report a change
1331+
// if we've "shifted" into 2nd/4th in the process of sequential
1332+
// shifting
1333+
changed = false;
1334+
1335+
// force neutral gear, ignoring the H-pattern selection
1336+
this->setGear(0);
1337+
1338+
// edge case: if we've not just switched into sequential mode,
1339+
// we need to ignore the H-pattern gear change (to 2/4, and then
1340+
// set by us to neutral). We can do that, hackily, by setting to
1341+
// neutral again to clear the cached gear for comparison.
1342+
if (this->sequentialProcess) {
1343+
this->setGear(0);
1344+
}
1345+
1346+
// read the raw y axis value, ignoring the H-pattern calibration
1347+
const int y = this->getPositionRaw(Axis::Y);
1348+
1349+
// save the previous state for reference
1350+
const int8_t prevState = this->sequentialState;
1351+
1352+
// if we're neutral, check for up/down shift
1353+
if (this->sequentialState == 0) {
1354+
if (y >= this->seqCalibration.upTrigger) this->sequentialState = 1;
1355+
else if (y <= this->seqCalibration.downTrigger) this->sequentialState = -1;
1356+
}
1357+
1358+
// if we're in up-shift mode, check for release
1359+
else if ((this->sequentialState == 1) && (y < this->seqCalibration.upRelease)) {
1360+
this->sequentialState = 0;
1361+
}
1362+
1363+
// if we're in down-shift mode, check for release
1364+
else if ((this->sequentialState == -1) && (y > this->seqCalibration.downRelease)) {
1365+
this->sequentialState = 0;
1366+
}
1367+
1368+
// set the 'changed' flag if the sequential state changed
1369+
if (prevState != this->sequentialState) {
1370+
changed = true;
1371+
}
1372+
// otherwise, set 'changed' based on the buttons *only*
1373+
else {
1374+
changed = this->buttonsChanged();
1375+
}
1376+
1377+
// set 'process' flag to handle edge case on subsequent updates
1378+
this->sequentialProcess = true;
1379+
}
1380+
1381+
// if we're not connected or if the sequential mode has been disabled,
1382+
// clear the sequential flags if they have been set
1383+
else {
1384+
if (this->sequentialProcess) {
1385+
this->sequentialProcess = false; // not in sequential mode
1386+
this->sequentialState = 0; // no sequential buttons pressed
1387+
changed = true;
1388+
}
1389+
}
1390+
1391+
return changed;
1392+
}
1393+
1394+
bool LogitechShifterG25::inSequentialMode() const {
1395+
return this->getButton(BUTTON_SEQUENTIAL);
1396+
}
1397+
1398+
bool LogitechShifterG25::getShiftUp() const {
1399+
return this->sequentialState == 1;
1400+
}
1401+
1402+
bool LogitechShifterG25::getShiftDown() const {
1403+
return this->sequentialState == -1;
1404+
}
1405+
1406+
void LogitechShifterG25::setCalibrationSequential(int neutral, int up, int down, float engagePoint, float releasePoint) {
1407+
// limit percentage thresholds
1408+
engagePoint = floatPercent(engagePoint);
1409+
releasePoint = floatPercent(releasePoint);
1410+
1411+
// prevent release point from being higher than engage
1412+
// (which will prevent the shifter from working at all)
1413+
if (releasePoint > engagePoint) {
1414+
releasePoint = engagePoint;
1415+
}
1416+
1417+
// calculate ranges
1418+
const int upRange = up - neutral;
1419+
const int downRange = neutral - down;
1420+
1421+
// calculate calibration points
1422+
this->seqCalibration.upTrigger = neutral + (upRange * engagePoint);
1423+
this->seqCalibration.upRelease = neutral + (upRange * releasePoint);
1424+
1425+
this->seqCalibration.downTrigger = neutral - (downRange * engagePoint);
1426+
this->seqCalibration.downRelease = neutral - (downRange * releasePoint);
1427+
}
1428+
1429+
void LogitechShifterG25::serialCalibrationSequential(Stream& iface) {
1430+
// err if not connected
1431+
if (this->isConnected() == false) {
1432+
iface.print(F("Error! Cannot perform calibration, "));
1433+
iface.print(F("shifter"));
1434+
iface.println(F(" is not connected."));
1435+
return;
1436+
}
1437+
1438+
const char* separator = "------------------------------------";
1439+
1440+
iface.println();
1441+
iface.println(F("Sim Racing Library G25 Sequential Shifter Calibration"));
1442+
iface.println(separator);
1443+
iface.println();
1444+
1445+
while (this->inSequentialMode() == false) {
1446+
iface.print(F("Please press down on the shifter and move the dial counter-clockwise to put the shifter into sequential mode"));
1447+
iface.print(F(". Send any character to continue."));
1448+
iface.println(F(" Send 'q' to quit."));
1449+
iface.println();
1450+
1451+
waitClient(iface);
1452+
this->update();
1453+
1454+
// quit if user sends 'q'
1455+
if (iface.read() == 'q') {
1456+
iface.println(F("Quitting sequential calibration! Goodbye <3"));
1457+
iface.println();
1458+
return;
1459+
}
1460+
1461+
// send an error if we're still not there
1462+
if (this->inSequentialMode() == false) {
1463+
iface.println(F("Error: The shifter is not in sequential mode"));
1464+
iface.println();
1465+
}
1466+
}
1467+
1468+
float engagementPoint = LogitechShifterG25::CalEngagementPoint;
1469+
float releasePoint = LogitechShifterG25::CalReleasePoint;
1470+
1471+
const uint8_t NumPoints = 3;
1472+
const char* directions[2] = {
1473+
"up",
1474+
"down",
1475+
};
1476+
int data[NumPoints];
1477+
1478+
int& neutral = data[0];
1479+
int& yMax = data[1];
1480+
int& yMin = data[2];
1481+
1482+
for (uint8_t i = 0; i < NumPoints; ++i) {
1483+
if (i == 0) {
1484+
iface.print(F("Leave the gear shifter in neutral"));
1485+
}
1486+
else {
1487+
iface.print(F("Please move the gear shifter to sequentially shift "));
1488+
iface.print(directions[i - 1]);
1489+
iface.print(F(" and hold it there"));
1490+
}
1491+
iface.println(F(". Send any character to continue."));
1492+
waitClient(iface);
1493+
1494+
this->update();
1495+
data[i] = this->getPositionRaw(Axis::Y);
1496+
iface.println(); // spacing
1497+
}
1498+
1499+
iface.println(F("These settings are optional. Send 'y' to customize. Send any other character to continue with the default values."));
1500+
1501+
iface.print(F(" * Shift Engagement Point: \t"));
1502+
iface.println(engagementPoint);
1503+
1504+
iface.print(F(" * Shift Release Point: \t"));
1505+
iface.println(releasePoint);
1506+
1507+
iface.println();
1508+
1509+
waitClient(iface);
1510+
1511+
if (iface.read() == 'y') {
1512+
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 shifting."));
1513+
readFloat(engagementPoint, iface);
1514+
iface.println();
1515+
1516+
iface.println(F("Set the release point as a floating point percentage. This is the percentage away from the neutral axis on Y to stop shifting. It must be less than the engagement point."));
1517+
readFloat(releasePoint, iface);
1518+
iface.println();
1519+
}
1520+
1521+
flushClient(iface);
1522+
1523+
// apply and print
1524+
this->setCalibrationSequential(neutral, yMax, yMin, engagementPoint, releasePoint);
1525+
1526+
iface.println(F("Here is your calibration:"));
1527+
iface.println(separator);
1528+
iface.println();
1529+
1530+
iface.print(F("shifter.setCalibrationSequential( "));
1531+
1532+
iface.print(neutral);
1533+
iface.print(", ");
1534+
iface.print(yMax);
1535+
iface.print(", ");
1536+
iface.print(yMin);
1537+
iface.print(", ");
1538+
1539+
iface.print(engagementPoint);
1540+
iface.print(", ");
1541+
iface.print(releasePoint);
1542+
iface.print(");");
1543+
iface.println();
1544+
1545+
iface.println();
1546+
iface.println(separator);
1547+
iface.println();
1548+
1549+
iface.println(F("Paste this line into the setup() function to calibrate on startup."));
1550+
iface.println(F("\n\nCalibration complete! :)\n"));
1551+
}
1552+
12881553
//#########################################################
12891554
// Handbrake #
12901555
//#########################################################

src/SimRacing.h

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1076,6 +1076,106 @@ namespace SimRacing {
10761076
uint16_t previousButtons; ///< the previous state of the buttons, for comparison
10771077
};
10781078

1079+
/**
1080+
* @brief Interface with the Logitech G25 shifter
1081+
* @ingroup Shifters
1082+
*
1083+
* The G25 shifter includes the same analog shifter as the Logitech Driving
1084+
* Force shifter (implemented in the LogitechShifter class), the buttons
1085+
* included with the G27 shifter (implemented in the LogitechShifterG27
1086+
* class), and a mode switch between H-pattern and sequential shift modes.
1087+
*
1088+
* @see https://en.wikipedia.org/wiki/Logitech_G25
1089+
*/
1090+
class LogitechShifterG25 : public LogitechShifterG27 {
1091+
public:
1092+
/**
1093+
* @copydoc LogitechShifterG27::LogitechShifterG27(PinNum, PinNum,
1094+
* PinNum, PinNum, PinNum, PinNum, PinNum)
1095+
*/
1096+
LogitechShifterG25(
1097+
PinNum pinX, PinNum pinY,
1098+
PinNum pinLatch, PinNum pinClock, PinNum pinData,
1099+
PinNum pinDetect = UnusedPin,
1100+
PinNum pinLed = UnusedPin
1101+
);
1102+
1103+
/** @copydoc LogitechShifterG27::begin() */
1104+
virtual void begin();
1105+
1106+
/**
1107+
* Check if the shifter is in sequential shifting mode
1108+
*
1109+
* @returns 'true' if the shifter is in sequential shifting mode,
1110+
* 'false' if the shifter is in H-pattern shifting mode.
1111+
*/
1112+
bool inSequentialMode() const;
1113+
1114+
/**
1115+
* Check if the sequential shifter is shifted up
1116+
*
1117+
* @returns 'true' if the sequential shifter is shifted up,
1118+
* 'false' otherwise
1119+
*/
1120+
bool getShiftUp() const;
1121+
1122+
/**
1123+
* Check if the sequential shifter is shifted down
1124+
*
1125+
* @returns 'true' if the sequential shifter is shifted down,
1126+
* 'false' otherwise
1127+
*/
1128+
bool getShiftDown() const;
1129+
1130+
/**
1131+
* Calibrate the sequential shifter for more accurate shifting.
1132+
*
1133+
* @param neutral the Y position of the shifter in neutral
1134+
* @param up the Y position of the shifter in sequential 'up'
1135+
* @param down the Y position of the shifter in sequential 'down'
1136+
* @param engagePoint distance from neutral on Y to register a gear as
1137+
* being engaged (as a percentage of distance from
1138+
* neutral to Y max, 0-1)
1139+
* @param releasePoint distance from neutral on Y to go back into neutral
1140+
* from an engaged gear (as a percentage of distance
1141+
* from neutral to Y max, 0-1)
1142+
*/
1143+
void setCalibrationSequential(int neutral, int up, int down, float engagePoint, float releasePoint);
1144+
1145+
/** @copydoc AnalogShifter::serialCalibration(Stream&) */
1146+
void serialCalibrationSequential(Stream& iface = Serial);
1147+
1148+
protected:
1149+
/** @copydoc Peripheral::updateState(bool) */
1150+
virtual bool updateState(bool connected);
1151+
1152+
private:
1153+
/**
1154+
* Distance from neutral on Y to register a gear as
1155+
* being engaged (as a percentage of distance from
1156+
* neutral to Y max, 0-1). Used for calibration.
1157+
*/
1158+
static const float CalEngagementPoint;
1159+
1160+
/**
1161+
* Distance from neutral on Y to go back into neutral
1162+
* from an engaged gear (as a percentage of distance
1163+
* from neutral to Y max, 0-1). Used for calibration.
1164+
*/
1165+
static const float CalReleasePoint;
1166+
1167+
bool sequentialProcess; ///< Flag to indicate whether we are processing sequential shifts
1168+
int8_t sequentialState; ///< Tri-state flag for the shift direction. 1 (Up), 0 (Neutral), -1 (Down).
1169+
1170+
/*** Internal calibration struct */
1171+
struct SequentialCalibration {
1172+
int upTrigger; ///< Threshold to set the sequential shift as 'up'
1173+
int upRelease; ///< Threshold to clear the 'up' sequential shift
1174+
int downTrigger; ///< Threshold to set the sequential shift as 'down'
1175+
int downRelease; ///< Threshold to clear the 'down' sequential shift
1176+
} seqCalibration;
1177+
};
1178+
10791179

10801180
#if defined(__AVR_ATmega32U4__) || defined(SIM_RACING_DOXYGEN)
10811181
/**

0 commit comments

Comments
 (0)