Arduino Sim Racing Library v1.1.4
Loading...
Searching...
No Matches
SimRacing.cpp
Go to the documentation of this file.
1/*
2 * Project Sim Racing Library for Arduino
3 * @author David Madison
4 * @link github.com/dmadison/Sim-Racing-Arduino
5 * @license LGPLv3 - Copyright (c) 2022 David Madison
6 *
7 * This file is part of the Sim Racing Library for Arduino.
8 *
9 * This program is free software: you can redistribute it and/or modify
10 * it under the terms of the GNU Lesser General Public License as published by
11 * the Free Software Foundation, either version 3 of the License, or
12 * (at your option) any later version.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU Lesser General Public License for more details.
18 *
19 * You should have received a copy of the GNU Lesser General Public License
20 * along with this program. If not, see <http://www.gnu.org/licenses/>.
21 */
22
23#include "SimRacing.h"
24
30namespace SimRacing {
31
32
43static constexpr long invertAxis(long value, long min, long max) {
44 return max - value + min; // flip to other side of the scale
45}
46
47
65static long remap(long value, long inMin, long inMax, long outMin, long outMax) {
66 // if inverted, swap min/max and adjust position of value
67 if (inMin > inMax) {
68 const long temp = inMin;
69 inMin = inMax;
70 inMax = temp;
71
72 value = invertAxis(value, inMin, inMax);
73 }
74
75 if (value <= inMin) return outMin;
76 if (value >= inMax) return outMax;
77 return map(value, inMin, inMax, outMin, outMax);
78}
79
80
87static float floatPercent(float pct) {
88 if (pct < 0.0) pct = 0.0;
89 else if (pct > 1.0) pct = 1.0;
90 return pct;
91}
92
101static void flushClient(Stream& client) {
102 while (client.read() != -1) { delay(2); } // 9600 baud = ~1 ms per byte
103}
104
110static void waitClient(Stream& client) {
111 flushClient(client);
112 while (client.peek() == -1) { delay(1); } // wait for a new byte (using delay to avoid watchdog)
113}
114
127static void readFloat(float& value, Stream& client) {
128 client.print("(to skip this step and go with the default value of '");
129 client.print(value);
130 client.print("', send 'n')");
131 client.println();
132
133 waitClient(client);
134 if (client.peek() == 'n') return; // skip this step
135
136 float input;
137
138 while (true) {
139 client.setTimeout(200);
140 input = client.parseFloat();
141
142 if (input >= 0.0 && input <= 1.0) {
143 client.print(F("Set the new value to '"));
144 client.print(input);
145 client.println("'");
146 break;
147 }
148 client.print(F("Input '"));
149 client.print(input);
150 client.print(F("' not within acceptable range (0.0 - 1.0). Please try again."));
151 client.println();
152
153 waitClient(client);
154 }
155
156 value = input;
157}
158
159
160//#########################################################
161// DeviceConnection #
162//#########################################################
163
164DeviceConnection::DeviceConnection(uint8_t pin, bool invert, unsigned long detectTime)
165 :
166 Pin(pin), Inverted(invert), stablePeriod(detectTime), // constants(ish)
167
168 /* Assume we're connected on first call
169 */
170 state(ConnectionState::Connected),
171
172 /* Init state to "not inverted", which is the connected state. For example
173 * if we're looking for 'HIGH' then inverted is false, which means the
174 * initial state is 'true' and thus connected.
175 *
176 * We're assuming we're connected on first call here because it allows
177 * the device to be read as connected as soon as the board turns on, without
178 * having to wait an arbitrary amount.
179 */
180 pinState(!Inverted),
181
182 /* Set the last pin change to right now minus the stable period so it's
183 * read as being already stable. Again, this will make the class return
184 * 'present' as soon as the board starts up
185 */
186 lastChange(millis() - detectTime)
187
188{
189 pinMode(Pin, INPUT); // set pin as input, *no* pull-up
190}
191
193 const bool newState = readPin();
194
195 if (newState == HIGH && state == ConnectionState::Connected) return; // short circuit, already connected
196
197 // check if the pin changed. if it did, record the time
198 if (pinState != newState) {
199 pinState = newState;
200 lastChange = millis();
201
202 // rising, we just connected
203 if (pinState == HIGH) {
205 }
206 // falling, we just disconnected
207 else {
209 }
210 }
211
212 // if pin hasn't changed, compare stable times
213 else {
214 // check stable connection (over time)
215 if (pinState == HIGH) {
216 const unsigned long now = millis();
217 if (now - lastChange >= stablePeriod) {
219 }
220 }
221 // if we were previously unplugged and are still low, now we're disconnected
222 else if (state == ConnectionState::Unplug) {
224 }
225 }
226}
227
231
233 return this->getState() == ConnectionState::Connected;
234}
235
237 stablePeriod = t;
238
239 if (state == ConnectionState::Connected) {
240 const unsigned long now = millis();
241
242 // if we were previously considered connected, adjust the timestamps
243 // accordingly so that we still are
244 if (now - lastChange < stablePeriod) {
245 lastChange = now - stablePeriod;
246 }
247 }
248}
249
250bool DeviceConnection::readPin() const {
251 if (Pin == NOT_A_PIN) return HIGH; // if no pin is set, we're always connected
252 const bool state = digitalRead(Pin);
253 return Inverted ? !state : state;
254}
255
256//#########################################################
257// AnalogInput #
258//#########################################################
259
260
262 : Pin(p), position(AnalogInput::Min), cal({AnalogInput::Min, AnalogInput::Max})
263{
264 if (Pin != NOT_A_PIN) {
265 pinMode(Pin, INPUT);
266 }
267}
268
270 bool changed = false;
271
272 if (Pin != NOT_A_PIN) {
273 const int previous = this->position;
274 this->position = analogRead(Pin);
275
276 // check if value is different for 'changed' flag
277 if (previous != this->position) {
278
279 const int rMin = isInverted() ? getMax() : getMin();
280 const int rMax = isInverted() ? getMin() : getMax();
281
282 if (
283 // if the previous value was under the minimum range
284 // and the current value is as well, no change
285 !(previous < rMin && this->position < rMin) &&
286
287 // if the previous value was over the maximum range
288 // and the current value is as well, no change
289 !(previous > rMax && this->position > rMax)
290 )
291 {
292 // otherwise, the current value is either within the
293 // range limits *or* it has changed from one extreme
294 // to the other. Either way, mark it changed!
295 changed = true;
296 }
297 }
298 }
299 return changed;
300}
301
302long AnalogInput::getPosition(long rMin, long rMax) const {
303 // inversion is handled within the remap function
304 return remap(getPositionRaw(), getMin(), getMax(), rMin, rMax);
305}
306
308 return this->position;
309}
310
312 return (this->cal.min > this->cal.max); // inverted if min is greater than max
313}
314
315void AnalogInput::setPosition(int newPos) {
316 this->position = newPos;
317}
318
319void AnalogInput::setInverted(bool invert) {
320 if (isInverted() == invert) return; // inversion already set
321
322 // to change inversion, swap max and min of the current calibration
323 AnalogInput::Calibration inverted = { this->cal.max, this->cal.min };
324 setCalibration(inverted);
325}
326
328 this->cal = newCal;
329}
330
331
332//#########################################################
333// Pedals #
334//#########################################################
335
336Pedals::Pedals(AnalogInput* dataPtr, uint8_t nPedals, uint8_t detectPin)
337 :
338 pedalData(dataPtr),
339 NumPedals(nPedals),
340 detector(detectPin),
341 changed(false)
342{}
343
345 update(); // set initial pedal position
346}
347
349 changed = false;
350
351 detector.poll();
352 if (detector.getState() == DeviceConnection::Connected) {
353 // if connected, read all pedal positions
354 for (int i = 0; i < getNumPedals(); ++i) {
355 changed |= pedalData[i].read();
356 }
357 }
358 else if (detector.getState() == DeviceConnection::Unplug) {
359 // on unplug event, zero all pedals
360 for (int i = 0; i < getNumPedals(); ++i) {
361 const int min = pedalData[i].getMin();
362 pedalData[i].setPosition(min);
363 }
364 changed = true; // set flag so we know everything moved to 0
365 }
366
367 return changed;
368}
369
370long Pedals::getPosition(PedalID pedal, long rMin, long rMax) const {
371 if (!hasPedal(pedal)) return rMin; // not a pedal
372 return pedalData[pedal].getPosition(rMin, rMax);
373}
374
376 if (!hasPedal(pedal)) return AnalogInput::Min; // not a pedal
377 return pedalData[pedal].getPositionRaw();
378}
379
380bool Pedals::hasPedal(PedalID pedal) const {
381 return (pedal < getNumPedals());
382}
383
385 if (!hasPedal(pedal)) return;
386 pedalData[pedal].setCalibration(cal);
387 pedalData[pedal].setPosition(pedalData[pedal].getMin()); // reset to min position
388}
389
391 String name;
392
393 switch (pedal) {
394 case(PedalID::Gas):
395 name = F("gas");
396 break;
397 case(PedalID::Brake):
398 name = F("brake");
399 break;
400 case(PedalID::Clutch):
401 name = F("clutch");
402 break;
403 default:
404 name = F("???");
405 break;
406 }
407 return name;
408}
409
410void Pedals::serialCalibration(Stream& iface) {
411 const char* separator = "------------------------------------";
412
413 iface.println();
414 iface.println(F("Sim Racing Library Pedal Calibration"));
415 iface.println(separator);
416 iface.println();
417
418 // read minimums
419 iface.println(F("Take your feet off of the pedals so they move to their resting position."));
420 iface.println(F("Send any character to continue."));
421 waitClient(iface);
422
423 const int MaxPedals = 3; // hard-coded at 3 pedals
424
425 AnalogInput::Calibration pedalCal[MaxPedals];
426
427 // read minimums
428 for (int i = 0; (i < getNumPedals()) && (i < MaxPedals); i++) {
429 pedalData[i].read(); // read position
430 pedalCal[i].min = pedalData[i].getPositionRaw(); // set min to the recorded position
431 }
432 iface.println(F("\nMinimum values for all pedals successfully recorded!\n"));
433 iface.println(separator);
434
435 // read maximums
436 iface.println(F("\nOne at a time, let's measure the maximum range of each pedal.\n"));
437 for (int i = 0; (i < getNumPedals()) && (i < MaxPedals); i++) {
438
439 iface.print(F("Push the "));
440 String name = getPedalName(static_cast<PedalID>(i));
441 name.toLowerCase();
442 iface.print(name);
443 iface.print(F(" pedal to the floor. "));
444
445 iface.println(F("Send any character to continue."));
446 waitClient(iface);
447
448 pedalData[i].read(); // read position
449 pedalCal[i].max = pedalData[i].getPositionRaw(); // set max to the recorded position
450 }
451
452 // deadzone options
453 iface.println(separator);
454 iface.println();
455
456 float DeadzoneMin = 0.01; // by default, 1% (trying to keep things responsive)
457 float DeadzoneMax = 0.025; // by default, 2.5%
458
459 iface.println(F("These settings are optional. Send 'y' to customize. Send any other character to continue with the default values."));
460
461 iface.print(F(" * Pedal Travel Deadzone, Start: \t"));
462 iface.print(DeadzoneMin);
463 iface.println(F(" (Used to avoid the pedal always being slightly pressed)"));
464
465 iface.print(F(" * Pedal Travel Deadzone, End: \t"));
466 iface.print(DeadzoneMax);
467 iface.println(F(" (Used to guarantee that the pedal can be fully pressed)"));
468
469 iface.println();
470
471 waitClient(iface);
472
473 if (iface.read() == 'y') {
474 iface.println(F("Set the pedal travel starting deadzone as a floating point percentage."));
475 readFloat(DeadzoneMin, iface);
476 iface.println();
477
478 iface.println(F("Set the pedal travel ending deadzone as a floating point percentage."));
479 readFloat(DeadzoneMax, iface);
480 iface.println();
481 }
482
483 flushClient(iface);
484
485 // calculate deadzone offsets
486 for (int i = 0; (i < getNumPedals()) && (i < MaxPedals); i++) {
487 auto &cMin = pedalCal[i].min;
488 auto &cMax = pedalCal[i].max;
489
490 const int range = abs(cMax - cMin);
491 const int dzMin = DeadzoneMin * (float)range;
492 const int dzMax = DeadzoneMax * (float)range;
493
494 // non-inverted
495 if (cMax >= cMin) {
496 cMax -= dzMax; // 'cut' into the range so it limits sooner
497 cMin += dzMin;
498 }
499 // inverted
500 else {
501 cMax += dzMax;
502 cMin -= dzMin;
503 }
504 }
505
506 // print finished calibration
507 iface.println(F("Here is your calibration:"));
508 iface.println(separator);
509 iface.println();
510
511 iface.print(F("pedals.setCalibration("));
512
513 for (int i = 0; (i < getNumPedals()) && (i < MaxPedals); i++) {
514 if(i > 0) iface.print(F(", "));
515 iface.print('{');
516
517 iface.print(pedalCal[i].min);
518 iface.print(F(", "));
519 iface.print(pedalCal[i].max);
520
521 iface.print('}');
522
523 this->setCalibration(static_cast<PedalID>(i), pedalCal[i]); // and set it ourselves, too
524 }
525 iface.print(");");
526 iface.println();
527
528 iface.println();
529 iface.println(separator);
530 iface.println();
531
532 iface.print(F("Paste this line into the setup() function. The "));
533 iface.print(F("pedals"));
534 iface.print(F(" will be calibrated with these values on startup."));
535 iface.println(F("\nCalibration complete! :)\n\n"));
536
537 flushClient(iface);
538}
539
540
541TwoPedals::TwoPedals(uint8_t gasPin, uint8_t brakePin, uint8_t detectPin)
542 : Pedals(pedalData, NumPedals, detectPin),
543 pedalData{ AnalogInput(gasPin), AnalogInput(brakePin) }
544{}
545
547 this->Pedals::setCalibration(PedalID::Gas, gasCal);
548 this->Pedals::setCalibration(PedalID::Brake, brakeCal);
549}
550
551
552ThreePedals::ThreePedals(uint8_t gasPin, uint8_t brakePin, uint8_t clutchPin, uint8_t detectPin)
553 : Pedals(pedalData, NumPedals, detectPin),
554 pedalData{ AnalogInput(gasPin), AnalogInput(brakePin), AnalogInput(clutchPin) }
555{}
556
558 this->Pedals::setCalibration(PedalID::Gas, gasCal);
559 this->Pedals::setCalibration(PedalID::Brake, brakeCal);
560 this->Pedals::setCalibration(PedalID::Clutch, clutchCal);
561}
562
563
564
565LogitechPedals::LogitechPedals(uint8_t gasPin, uint8_t brakePin, uint8_t clutchPin, uint8_t detectPin)
566 : ThreePedals(gasPin, brakePin, clutchPin, detectPin)
567{
568 // taken from calibrating my own pedals. the springs are pretty stiff so while
569 // this covers the whole travel range, users may want to back it down for casual
570 // use (esp. for the brake travel)
571 this->setCalibration({ 904, 48 }, { 944, 286 }, { 881, 59 });
572}
573
574LogitechDrivingForceGT_Pedals::LogitechDrivingForceGT_Pedals(uint8_t gasPin, uint8_t brakePin, uint8_t detectPin)
575 : TwoPedals(gasPin, brakePin, detectPin)
576{
577 this->setCalibration({ 646, 0 }, { 473, 1023 }); // taken from calibrating my own pedals
578}
579
580
581//#########################################################
582// Shifter #
583//#########################################################
584
585Shifter::Shifter(int8_t min, int8_t max)
586 : MinGear(min), MaxGear(max)
587{}
588
589char Shifter::getGearChar(int gear) {
590 char c = '?';
591
592 switch (gear) {
593 case(-1):
594 c = 'r';
595 break;
596 case(0):
597 c = 'n';
598 break;
599 default:
600 if (gear > 0 && gear <= 9)
601 c = '0' + gear;
602 break;
603 }
604 return c;
605}
606
608 return getGearChar(getGear());
609}
610
611String Shifter::getGearString(int gear) {
612 String name;
613
614 switch (gear) {
615 case(-1):
616 name = F("reverse");
617 break;
618 case(0):
619 name = F("neutral");
620 break;
621 default: {
622 if (gear < 0 || gear > 9) {
623 name = F("???");
624 break; // out of range
625 }
626 name = gear; // set string to current gear
627
628 switch (gear) {
629 case(1):
630 name += F("st");
631 break;
632 case(2):
633 name += F("nd");
634 break;
635 case(3):
636 name += F("rd");
637 break;
638 default:
639 name += F("th");
640 break;
641 }
642 break;
643 }
644 }
645 return name;
646}
647
649 return getGearString(getGear());
650}
651
652
653/* Static calibration constants
654* These values are arbitrary - just what worked well with my own shifter.
655*/
656const float AnalogShifter::CalEngagementPoint = 0.70;
657const float AnalogShifter::CalReleasePoint = 0.50;
658const float AnalogShifter::CalEdgeOffset = 0.60;
659
660AnalogShifter::AnalogShifter(uint8_t pinX, uint8_t pinY, uint8_t pinRev, uint8_t detectPin)
661 :
662 /* In initializing the Shifter, the lowest gear is going to be '-1' if a pin
663 * exists for reverse, otherwise it's going to be '0' (neutral).
664 */
665 Shifter(pinRev != NOT_A_PIN ? -1 : 0, 6),
666
667 /* Two axes, X and Y */
668 analogAxis{ AnalogInput(pinX), AnalogInput(pinY) },
669
670 PinReverse(pinRev),
671 detector(detectPin, false) // not inverted
672{}
673
675 pinMode(PinReverse, INPUT);
676 update(); // set initial gear position
677}
678
680 detector.poll();
681
682 switch (detector.getState()) {
683
684 // connected! poll the ADC for new analog axis data
686 analogAxis[Axis::X].read();
687 analogAxis[Axis::Y].read();
688 break;
689
690 // on an unplug event, we want to reset our position back to
691 // neutral and then immediately return
693 {
694 const int8_t previousGear = this->getGear();
695
696 analogAxis[Axis::X].setPosition(calibration.neutralX);
697 analogAxis[Axis::Y].setPosition(calibration.neutralY);
698
699 if (previousGear != 0) changed = true;
700 currentGear = 0;
701 return changed;
702 break;
703 }
704
705 // if the device is either disconnected or just plugged in and unstable, set gear
706 // 'changed' to false and then immediately return false to save on processing
709 changed = false;
710 return changed;
711 break;
712 }
713
714 const int8_t previousGear = this->getGear();
715 const bool prevOdd = ((previousGear != -1) && (previousGear & 1)); // were we previously in an odd gear
716 const bool prevEven = (!prevOdd && previousGear != 0); // were we previously in an even gear
717
718 const int x = analogAxis[Axis::X].getPosition();
719 const int y = analogAxis[Axis::Y].getPosition();
720 int8_t newGear = 0;
721
722 // If we're below the 'release' thresholds, we must still be in the previous gear
723 if ((prevOdd && y > calibration.oddRelease) || (prevEven && y < calibration.evenRelease)) {
724 newGear = previousGear;
725 }
726
727 // If we're *not* below the release thresholds, we may be in a different gear
728 else {
729 // Check if we're in even or odd gears (Y axis)
730 if (y > calibration.oddTrigger) {
731 newGear = 1; // we're in an odd gear
732 }
733 else if (y < calibration.evenTrigger) {
734 newGear = 2; // we're in an even gear
735 }
736
737 if (newGear != 0) {
738 // Now check *which* gear we're in, if we're in one (X axis)
739 if (x > calibration.rightEdge) newGear += 4; // 1-2 + 4 = 5-6
740 else if (x >= calibration.leftEdge) newGear += 2; // 1-2 + 2 = 3-4
741 // (note the '>=', because it would normally be a '<' check for the lower range)
742 // else gear = 1-2 (as set above)
743
744 const bool reverse = getReverseButton();
745
746 // If the reverse button is pressed and we're in 5th gear
747 // something is wrong. Revert that and go back to neutral.
748 if (reverse && newGear == 5) {
749 newGear = 0;
750 }
751
752 // If the reverse button is pressed or we were previously
753 // in reverse *and* we are currently in 6th gear, then we
754 // should be in reverse.
755 else if ((reverse || previousGear == -1) && newGear == 6) {
756 newGear = -1;
757 }
758 }
759 }
760
761 changed = (newGear != previousGear) ? 1 : 0;
762 currentGear = newGear;
763
764 return changed;
765}
766
767long AnalogShifter::getPosition(Axis ax, long min, long max) const {
768 if (ax != Axis::X && ax != Axis::Y) return min; // not an axis
769 return analogAxis[ax].getPosition(min, max);
770}
771
773 if (ax != Axis::X && ax != Axis::Y) return AnalogInput::Min; // not an axis
774 return analogAxis[ax].getPositionRaw();
775}
776
778 // if the reverse pin is not set *or* if the device is not currently
779 // connected, avoid reading the floating input and just return 'false'
780 if (PinReverse == NOT_A_PIN || detector.getState() != DeviceConnection::Connected) {
781 return false;
782 }
783 return digitalRead(PinReverse);
784}
785
787 GearPosition neutral,
789 float engagePoint, float releasePoint, float edgeOffset) {
790
791 // limit percentage thresholds
792 engagePoint = floatPercent(engagePoint);
793 releasePoint = floatPercent(releasePoint);
794 edgeOffset = floatPercent(edgeOffset);
795
796 const int xLeft = (g1.x + g2.x) / 2; // find the minimum X position average
797 const int xRight = (g5.x + g6.x) / 2; // find the maximum X position average
798
799 const int yOdd = (g1.y + g3.y + g5.y) / 3; // find the maximum Y position average
800 const int yEven = (g2.y + g4.y + g6.y) / 3; // find the minimum Y position average
801
802 // set X/Y calibration and inversion
803 analogAxis[Axis::X].setCalibration({ xLeft, xRight });
804 analogAxis[Axis::Y].setCalibration({ yEven, yOdd });
805
806 // save neutral values (raw)
807 calibration.neutralX = neutral.x;
808 calibration.neutralY = neutral.y;
809
810 // get normalized and inverted neutral values
811 // this lets us take advantage of the AnalogInput normalization function
812 // that handles inverted axes and automatic range rescaling, so the rest of
813 // the calibration options can be in the normalized range
814 const Axis axes[2] = { Axis::X, Axis::Y };
815 int* const neutralAxis[2] = { &neutral.x, &neutral.y };
816 for (int i = 0; i < 2; i++) {
817 const int previous = analogAxis[axes[i]].getPositionRaw(); // save current value
818 analogAxis[axes[i]].setPosition(*neutralAxis[i]); // set new value to neutral calibration
819 *neutralAxis[i] = analogAxis[axes[i]].getPosition(); // get normalized neutral value
820 analogAxis[axes[i]].setPosition(previous); // reset axis position to previous
821 }
822
823 // calculate the distances between each neutral and the limits of each axis
824 const int yOddDiff = AnalogInput::Max - neutral.y;
825 const int yEvenDiff = neutral.y - AnalogInput::Min;
826
827 const int leftDiff = neutral.x - AnalogInput::Min;
828 const int rightDiff = AnalogInput::Max - neutral.x;
829
830 // calculate and save the trigger and release points for each level
831 calibration.oddTrigger = neutral.y + ((float)yOddDiff * engagePoint);
832 calibration.oddRelease = neutral.y + ((float)yOddDiff * releasePoint);
833
834 calibration.evenTrigger = neutral.y - ((float)yEvenDiff * engagePoint);
835 calibration.evenRelease = neutral.y - ((float)yEvenDiff * releasePoint);
836
837 calibration.leftEdge = neutral.x - ((float)leftDiff * edgeOffset);
838 calibration.rightEdge = neutral.x + ((float)rightDiff * edgeOffset);
839
840#if 0
841 Serial.print("Odd Trigger: ");
842 Serial.println(calibration.oddTrigger);
843 Serial.print("Odd Release: ");
844 Serial.println(calibration.oddRelease);
845 Serial.print("Even Trigger: ");
846 Serial.println(calibration.evenTrigger);
847 Serial.print("Even Release: ");
848 Serial.println(calibration.evenRelease);
849 Serial.print("Left Edge: ");
850 Serial.println(calibration.leftEdge);
851 Serial.print("Right Edge: ");
852 Serial.println(calibration.rightEdge);
853 Serial.println();
854
855 Serial.print("X Min: ");
856 Serial.println(analogAxis[Axis::X].getMin());
857 Serial.print("X Max: ");
858 Serial.println(analogAxis[Axis::X].getMax());
859
860 Serial.print("Y Min: ");
861 Serial.println(analogAxis[Axis::Y].getMin());
862 Serial.print("Y Max: ");
863 Serial.println(analogAxis[Axis::Y].getMax());
864#endif
865}
866
868 if (isConnected() == false) {
869 iface.print(F("Error! Cannot perform calibration, "));
870 iface.print(F("shifter"));
871 iface.println(F(" is not connected."));
872 return;
873 }
874
875 const char* separator = "------------------------------------";
876
877 iface.println();
878 iface.println(F("Sim Racing Library Shifter Calibration"));
879 iface.println(separator);
880 iface.println();
881
882 AnalogShifter::GearPosition gears[7]; // neutral, then 1-6
883 float engagementPoint = CalEngagementPoint;
884 float releasePoint = CalReleasePoint;
885 float edgeOffset = CalEdgeOffset;
886
887 for (int i = 0; i <= 6; i++) {
888 const String gearName = this->getGearString(i);
889
890 iface.print(F("Please move the gear shifter into "));
891 iface.print(gearName);
892 iface.println(F(". Send any character to continue."));
893
894 waitClient(iface);
895
896 this->update();
897 gears[i] = {
898 this->analogAxis[Axis::X].getPositionRaw(),
899 this->analogAxis[Axis::Y].getPositionRaw()
900 };
901
902 iface.print("Gear '");
903 iface.print(gearName);
904 iface.print("' position recorded as { ");
905 iface.print(gears[i].x);
906 iface.print(", ");
907 iface.print(gears[i].y);
908 iface.println(" }");
909 iface.println();
910 }
911
912 iface.println(separator);
913 iface.println();
914
915 iface.println(F("These settings are optional. Send 'y' to customize. Send any other character to continue with the default values."));
916
917 iface.print(F(" * Gear Engagement Point: \t"));
918 iface.println(engagementPoint);
919
920 iface.print(F(" * Gear Release Point: \t"));
921 iface.println(releasePoint);
922
923 iface.print(F(" * Horizontal Gate Offset:\t"));
924 iface.println(edgeOffset);
925
926 iface.println();
927
928 waitClient(iface);
929
930 if (iface.read() == 'y') {
931 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."));
932 readFloat(engagementPoint, iface);
933 iface.println();
934
935 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."));
936 readFloat(releasePoint, iface);
937 iface.println();
938
939 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."));
940 readFloat(edgeOffset, iface);
941 iface.println();
942 }
943
944 flushClient(iface);
945
946 this->setCalibration(gears[0], gears[1], gears[2], gears[3], gears[4], gears[5], gears[6], engagementPoint, releasePoint, edgeOffset);
947
948 iface.println(F("Here is your calibration:"));
949 iface.println(separator);
950 iface.println();
951
952 iface.print(F("shifter.setCalibration( "));
953
954 for (int i = 0; i < 7; i++) {
955 iface.print('{');
956
957 iface.print(gears[i].x);
958 iface.print(", ");
959 iface.print(gears[i].y);
960
961 iface.print('}');
962 iface.print(", ");
963 }
964 iface.print(engagementPoint);
965 iface.print(", ");
966 iface.print(releasePoint);
967 iface.print(", ");
968 iface.print(edgeOffset);
969 iface.print(");");
970 iface.println();
971
972 iface.println();
973 iface.println(separator);
974 iface.println();
975
976 iface.println(F("Paste this line into the setup() function to calibrate on startup."));
977 iface.println(F("\n\nCalibration complete! :)\n"));
978}
979
980LogitechShifter::LogitechShifter(uint8_t pinX, uint8_t pinY, uint8_t pinRev, uint8_t detectPin)
981 : AnalogShifter(pinX, pinY, pinRev, detectPin)
982{
983 this->setCalibration({ 490, 440 }, { 253, 799 }, { 262, 86 }, { 460, 826 }, { 470, 76 }, { 664, 841 }, { 677, 77 });
984}
985
986//#########################################################
987// Handbrake #
988//#########################################################
989
990Handbrake::Handbrake(uint8_t pinAx, uint8_t detectPin)
991 :
992 analogAxis(pinAx),
993 detector(detectPin),
994 changed(false)
995{}
996
998 update(); // set initial handbrake position
999}
1000
1002 changed = false;
1003
1004 detector.poll();
1005 if (detector.getState() == DeviceConnection::Connected) {
1006 changed = analogAxis.read();
1007 }
1008 else if (detector.getState() == DeviceConnection::Unplug) {
1009 analogAxis.setPosition(analogAxis.getMin());
1010 changed = true;
1011 }
1012
1013 return changed;
1014}
1015
1016long Handbrake::getPosition(long rMin, long rMax) const {
1017 return analogAxis.getPosition(rMin, rMax);
1018}
1019
1021 return analogAxis.getPositionRaw();
1022}
1023
1025 analogAxis.setCalibration(newCal);
1026 analogAxis.setPosition(analogAxis.getMin()); // reset to min
1027}
1028
1029void Handbrake::serialCalibration(Stream& iface) {
1030 if (isConnected() == false) {
1031 iface.print(F("Error! Cannot perform calibration, "));
1032 iface.print(F("handbrake"));
1033 iface.println(F(" is not connected."));
1034 return;
1035 }
1036
1037 const char* separator = "------------------------------------";
1038
1039 iface.println();
1040 iface.println(F("Sim Racing Library Handbrake Calibration"));
1041 iface.println(separator);
1042 iface.println();
1043
1045
1046 // read minimum
1047 iface.println(F("Keep your hand off of the handbrake to record its resting position"));
1048 iface.println(F("Send any character to continue."));
1049 waitClient(iface);
1050
1051 analogAxis.read();
1052 newCal.min = analogAxis.getPositionRaw();
1053 iface.println();
1054
1055 // read maximum
1056 iface.println(F("Now pull on the handbrake and hold it at the end of its range"));
1057 iface.println(F("Send any character to continue."));
1058 waitClient(iface);
1059
1060 analogAxis.read();
1061 newCal.max = analogAxis.getPositionRaw();
1062 iface.println();
1063
1064 // set new calibration
1065 this->setCalibration(newCal);
1066
1067 // print finished calibration
1068 iface.println(F("Here is your calibration:"));
1069 iface.println(separator);
1070 iface.println();
1071
1072 iface.print(F("handbrake.setCalibration("));
1073 iface.print('{');
1074 iface.print(newCal.min);
1075 iface.print(F(", "));
1076 iface.print(newCal.max);
1077 iface.print("});");
1078 iface.println();
1079
1080 iface.println();
1081 iface.println(separator);
1082 iface.println();
1083
1084 iface.print(F("Paste this line into the setup() function. The "));
1085 iface.print(F("handbrake"));
1086 iface.print(F(" will be calibrated with these values on startup."));
1087 iface.println(F("\nCalibration complete! :)\n\n"));
1088
1089 flushClient(iface);
1090}
1091
1092}; // end SimRacing namespace
Header file for the Sim Racing Library.
Axis
Enumeration for analog axis names, mapped to integers.
Definition SimRacing.h:37
Handle I/O for analog (ADC) inputs.
Definition SimRacing.h:124
void setInverted(bool invert=true)
Set the 'inverted' state of the axis.
bool isInverted() const
Check whether the axis is inverted or not.
long getPosition(long rMin=Min, long rMax=Max) const
Retrieves the buffered position for the analog axis, rescaled to a nominal range using the calibratio...
int getPositionRaw() const
Retrieves the buffered position for the analog axis.
static const int Max
Maximum value of the analog to digital (ADC) converter. 10-bit by default.
Definition SimRacing.h:127
int getMax() const
Retrieves the calibrated maximum position.
Definition SimRacing.h:176
AnalogInput(uint8_t p)
Class constructor.
void setCalibration(Calibration newCal)
Calibrate the axis' min/max values for rescaling.
virtual bool read()
Updates the current value of the axis by polling the ADC.
int getMin() const
Retrieves the calibrated minimum position.
Definition SimRacing.h:169
void setPosition(int newPos)
Override the current position with a custom value.
static const int Min
Minimum value of the analog to digital (ADC) converter.
Definition SimRacing.h:126
Interface with shifters using two potentiometers for gear position.
Definition SimRacing.h:535
void setCalibration(GearPosition neutral, GearPosition g1, GearPosition g2, GearPosition g3, GearPosition g4, GearPosition g5, GearPosition g6, float engagePoint=CalEngagementPoint, float releasePoint=CalReleasePoint, float edgeOffset=CalEdgeOffset)
Calibrate the gear shifter for more accurate shifting.
void serialCalibration(Stream &iface=Serial)
Runs an interactive calibration tool using the serial interface.
bool getReverseButton() const
Checks the current state of the "reverse" button at the bottom of the shift column.
AnalogShifter(uint8_t pinX, uint8_t pinY, uint8_t pinRev=NOT_A_PIN, uint8_t detectPin=NOT_A_PIN)
Class constructor.
int getPositionRaw(Axis ax) const
Retrieves the buffered position for the analog axis.
virtual bool update()
Polls the hardware to update the current gear state.
bool isConnected() const
Check if the device is physically connected to the board.
Definition SimRacing.h:629
virtual void begin()
Initializes the hardware pins for reading the gear states.
long getPosition(Axis ax, long rMin=AnalogInput::Min, long rMax=AnalogInput::Max) const
Retrieves the buffered position for the analog axis, rescaled to a nominal range using the calibratio...
void poll()
Checks if the pin detects a connection.
void setStablePeriod(unsigned long t)
Allows the user to change the stable period of the detector.
DeviceConnection(uint8_t pin, bool invert=false, unsigned long detectTime=250)
Class constructor.
ConnectionState getState() const
Retrieves the current ConnectionState from the instance.
ConnectionState
The state of the connection, whether it is connected, disconnected, and everywhere in-between.
Definition SimRacing.h:56
@ Unplug
Device was just removed (connection ends)
Definition SimRacing.h:60
@ PlugIn
Device was just plugged in (connection starts), unstable.
Definition SimRacing.h:58
@ Disconnected
No connection present.
Definition SimRacing.h:57
@ Connected
Connection present and stable.
Definition SimRacing.h:59
bool isConnected() const
Check if the device is physically connected to the board.
virtual void begin()
Initializes the pin for reading from the handbrake.
long getPosition(long rMin=0, long rMax=100) const
Retrieves the buffered position for the handbrake axis, rescaled to a nominal range using the calibra...
void setCalibration(AnalogInput::Calibration newCal)
Calibrate the axis' min/max values for rescaling.
Handbrake(uint8_t pinAx, uint8_t detectPin=NOT_A_PIN)
Class constructor.
int getPositionRaw() const
Retrieves the buffered position for the handbrake, ignoring the calibration data.
virtual bool update()
Polls the handbrake to update its position.
bool isConnected() const
Check if the device is physically connected to the board.
Definition SimRacing.h:733
void serialCalibration(Stream &iface=Serial)
Runs an interactive calibration tool using the serial interface.
LogitechDrivingForceGT_Pedals(uint8_t gasPin, uint8_t brakePin, uint8_t detectPin=NOT_A_PIN)
Class constructor.
LogitechPedals(uint8_t gasPin, uint8_t brakePin, uint8_t clutchPin, uint8_t detectPin=NOT_A_PIN)
Class constructor.
LogitechShifter(uint8_t pinX, uint8_t pinY, uint8_t pinRev=NOT_A_PIN, uint8_t detectPin=NOT_A_PIN)
Class constructor.
Base class for all pedals instances.
Definition SimRacing.h:276
void serialCalibration(Stream &iface=Serial)
Runs an interactive calibration tool using the serial interface.
void setCalibration(PedalID pedal, AnalogInput::Calibration cal)
Calibrate a pedal's min/max values for rescaling.
Pedals(AnalogInput *dataPtr, uint8_t nPedals, uint8_t detectPin)
Class constructor.
static String getPedalName(PedalID pedal)
Utility function to get the string name for each pedal.
bool hasPedal(PedalID pedal) const
Checks if a given pedal is present in the class.
int getPositionRaw(PedalID pedal) const
Retrieves the buffered position for the pedal, ignoring the calibration data.
virtual bool update()
Perform a poll of the hardware to refresh the class state.
virtual void begin()
Initialize the hardware (if necessary)
long getPosition(PedalID pedal, long rMin=0, long rMax=100) const
Retrieves the buffered position for the pedal, rescaled to a nominal range using the calibration valu...
int getNumPedals() const
Retrieves the number of pedals handled by the class.
Definition SimRacing.h:332
Base class for all shifter instances.
Definition SimRacing.h:445
String getGearString() const
Returns a String that represents the current gear.
int8_t getGear() const
Returns the currently selected gear.
Definition SimRacing.h:463
Shifter(int8_t min, int8_t max)
Class constructor.
char getGearChar() const
Returns a character that represents the current gear.
bool changed
whether the gear has changed since the previous update
Definition SimRacing.h:528
int8_t currentGear
index of the current gear
Definition SimRacing.h:527
Pedal implementation for devices with gas, brake, and clutch.
Definition SimRacing.h:407
ThreePedals(uint8_t gasPin, uint8_t brakePin, uint8_t clutchPin, uint8_t detectPin=NOT_A_PIN)
Class constructor.
void setCalibration(AnalogInput::Calibration gasCal, AnalogInput::Calibration brakeCal, AnalogInput::Calibration clutchCal)
Sets the calibration data (min/max) for the pedals.
Pedal implementation for devices with only gas and brake.
Definition SimRacing.h:379
void setCalibration(AnalogInput::Calibration gasCal, AnalogInput::Calibration brakeCal)
Sets the calibration data (min/max) for the pedals.
TwoPedals(uint8_t gasPin, uint8_t brakePin, uint8_t detectPin=NOT_A_PIN)
Class constructor.
Pedal
Pedal ID names.
Definition SimRacing.h:265
Simple struct containing min/max values for axis calibration.
Definition SimRacing.h:215
int max
Maximum value of the analog axis.
Definition SimRacing.h:217
int min
Minimum value of the analog axis.
Definition SimRacing.h:216
Simple struct to store X/Y coordinates for the calibration function.
Definition SimRacing.h:583
int y
Y coordinate of the gear position from the ADC.
Definition SimRacing.h:585
int x
X coordinate of the gear position from the ADC.
Definition SimRacing.h:584