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