Arduino Sim Racing Library v2.0.0
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#if defined(__AVR_ATmega32U4__) || defined(SIM_RACING_DOXYGEN)
33
34template<>
35LogitechPedals CreateShieldObject<LogitechPedals, 1>() {
36 // Power (VCC): DE-9 pin 9, bridged to DE-9 pin 6
37 // Ground (GND): DE-9 pin 1
38
39 const PinNum Pin_Gas = A2; // DE-9 pin 2
40 const PinNum Pin_Brake = A1; // DE-9 pin 3
41 const PinNum Pin_Clutch = A0; // DE-9 pin 4
42 const PinNum Pin_Detect = 10; // DE-9 pin 6, requires 10k Ohm pull-down
43
44 return LogitechPedals(Pin_Gas, Pin_Brake, Pin_Clutch, Pin_Detect);
45}
46
47template<>
48LogitechPedals CreateShieldObject<LogitechPedals, 2>() {
49 // version 2 of the pedals shield has the same pinout,
50 // so we can use the v1 function
51 return CreateShieldObject<LogitechPedals, 1>();
52}
53
54template<>
55LogitechShifter CreateShieldObject<LogitechShifter, 1>() {
56 // Power (VCC): DE-9 pin 9, bridged to DE-9 pin 7
57 // Ground (GND): DE-9 pin 6
58 // DE-9 pin 3 (CS) needs to be pulled-up to VCC
59
60 const PinNum Pin_X_Wiper = A1; // DE-9 pin 4
61 const PinNum Pin_Y_Wiper = A0; // DE-9 pin 8
62 const PinNum Pin_DataOut = 14; // DE-9 pin 2
63 const PinNum Pin_Detect = A2; // DE-9 pin 7, requires 10k Ohm pull-down
64
65 return LogitechShifter(Pin_X_Wiper, Pin_Y_Wiper, Pin_DataOut, Pin_Detect);
66}
67
68template<>
69LogitechShifter CreateShieldObject<LogitechShifter, 2>() {
70 // version 2 of the shifter shield has the same data pinout for
71 // the Driving Force shifter, so we can use the v1 function
72 return CreateShieldObject<LogitechShifter, 1>();
73}
74
75template<>
76LogitechShifterG27 CreateShieldObject<LogitechShifterG27, 2>() {
77 // Power (VCC): DE-9 pin 9, bridged to DE-9 pin 7
78 // Ground (GND): DE-9 pin 6
79
80 const PinNum Pin_X_Wiper = A1; // DE-9 pin 4
81 const PinNum Pin_Y_Wiper = A0; // DE-9 pin 8
82 const PinNum Pin_DataOut = 14; // DE-9 pin 2
83
84 const PinNum Pin_Latch = 10; // DE-9 pin 3, aka chip select, requires 10k Ohm pull-up
85 const PinNum Pin_Clock = 15; // DE-9 pin 1, should have 470 Ohm resistor to prevent shorts
86
87 const PinNum Pin_LED = 16; // DE-9 pin 5, has a 100-120 Ohm series resistor
88 const PinNum Pin_Detect = A2; // DE-9 pin 7, requires 10k Ohm pull-down
89
90 return LogitechShifterG27(Pin_X_Wiper, Pin_Y_Wiper, Pin_Latch, Pin_Clock, Pin_DataOut, Pin_LED, Pin_Detect);
91}
92
93template<>
94LogitechShifterG25 CreateShieldObject<LogitechShifterG25, 2>() {
95 // Power (VCC): DE-9 pin 9, bridged to DE-9 pin 1
96 // Ground (GND): DE-9 pin 6
97
98 const PinNum Pin_X_Wiper = A1; // DE-9 pin 4
99 const PinNum Pin_Y_Wiper = A0; // DE-9 pin 8
100 const PinNum Pin_DataOut = 14; // DE-9 pin 2
101
102 const PinNum Pin_Latch = 10; // DE-9 pin 3, aka chip select, requires 10k Ohm pull-up
103 const PinNum Pin_Clock = A2; // DE-9 pin 7, should have 470 Ohm resistor to prevent shorts
104
105 const PinNum Pin_LED = 16; // DE-9 pin 5, has a 100-120 Ohm series resistor
106 const PinNum Pin_Detect = 15; // DE-9 pin 1, requires 10k Ohm pull-down
107
108 return LogitechShifterG25(Pin_X_Wiper, Pin_Y_Wiper, Pin_Latch, Pin_Clock, Pin_DataOut, Pin_LED, Pin_Detect);
109}
110#endif // ATmega32U4 for shield functions
111
112
124static constexpr PinNum sanitizePin(PinNum pin) {
125 return pin < 0 ? UnusedPin : pin;
126}
127
128
139static constexpr long invertAxis(long value, long min, long max) {
140 return max - value + min; // flip to other side of the scale
141}
142
143
161static long remap(long value, long inMin, long inMax, long outMin, long outMax) {
162 // if inverted, swap min/max and adjust position of value
163 if (inMin > inMax) {
164 const long temp = inMin;
165 inMin = inMax;
166 inMax = temp;
167
168 value = invertAxis(value, inMin, inMax);
169 }
170
171 if (value <= inMin) return outMin;
172 if (value >= inMax) return outMax;
173 return map(value, inMin, inMax, outMin, outMax);
174}
175
176
183static float floatPercent(float pct) {
184 if (pct < 0.0) pct = 0.0;
185 else if (pct > 1.0) pct = 1.0;
186 return pct;
187}
188
197static void flushClient(Stream& client) {
198 while (client.read() != -1) { delay(2); } // 9600 baud = ~1 ms per byte
199}
200
206static void waitClient(Stream& client) {
207 flushClient(client);
208 while (client.peek() == -1) { delay(1); } // wait for a new byte (using delay to avoid watchdog)
209}
210
223static void readFloat(float& value, Stream& client) {
224 client.print("(to skip this step and go with the default value of '");
225 client.print(value);
226 client.print("', send 'n')");
227 client.println();
228
229 waitClient(client);
230 if (client.peek() == 'n') return; // skip this step
231
232 float input;
233
234 while (true) {
235 client.setTimeout(200);
236 input = client.parseFloat();
237
238 if (input >= 0.0 && input <= 1.0) {
239 client.print(F("Set the new value to '"));
240 client.print(input);
241 client.println("'");
242 break;
243 }
244 client.print(F("Input '"));
245 client.print(input);
246 client.print(F("' not within acceptable range (0.0 - 1.0). Please try again."));
247 client.println();
248
249 waitClient(client);
250 }
251
252 value = input;
253}
254
255
256//#########################################################
257// DeviceConnection #
258//#########################################################
259
260DeviceConnection::DeviceConnection(PinNum pin, bool activeLow, unsigned long detectTime)
261 :
262 pin(sanitizePin(pin)), inverted(activeLow), stablePeriod(detectTime), // constants(ish)
263
264 /* Assume we're connected on first call
265 */
266 state(ConnectionState::Connected),
267
268 /* Init state to "not inverted", which is the connected state. For example
269 * if we're looking for 'HIGH' then inverted is false, which means the
270 * initial state is 'true' and thus connected.
271 *
272 * We're assuming we're connected on first call here because it allows
273 * the device to be read as connected as soon as the board turns on, without
274 * having to wait an arbitrary amount.
275 */
276 pinState(!inverted),
277
278 /* Set the last pin change to right now minus the stable period so it's
279 * read as being already stable. Again, this will make the class return
280 * 'present' as soon as the board starts up
281 */
282 lastChange(millis() - detectTime)
283
284{
285 if (pin != UnusedPin) {
286 pinMode(pin, INPUT); // set pin as input, *no* pull-up
287 }
288}
289
291 const bool newState = readPin();
292
293 if (newState == HIGH && state == ConnectionState::Connected) return; // short circuit, already connected
294
295 // check if the pin changed. if it did, record the time
296 if (pinState != newState) {
297 pinState = newState;
298 lastChange = millis();
299
300 // rising, we just connected
301 if (pinState == HIGH) {
303 }
304 // falling, we just disconnected
305 else {
307 }
308 }
309
310 // if pin hasn't changed, compare stable times
311 else {
312 // check stable connection (over time)
313 if (pinState == HIGH) {
314 const unsigned long now = millis();
315 if (now - lastChange >= stablePeriod) {
317 }
318 }
319 // if we were previously unplugged and are still low, now we're disconnected
320 else if (state == ConnectionState::Unplug) {
322 }
323 }
324}
325
329
331 return this->getState() == ConnectionState::Connected;
332}
333
335 stablePeriod = t;
336
337 if (state == ConnectionState::Connected) {
338 const unsigned long now = millis();
339
340 // if we were previously considered connected, adjust the timestamps
341 // accordingly so that we still are
342 if (now - lastChange < stablePeriod) {
343 lastChange = now - stablePeriod;
344 }
345 }
346}
347
348bool DeviceConnection::readPin() const {
349 if (pin == UnusedPin) return HIGH; // if no pin is set, we're always connected
350 const bool state = digitalRead(pin);
351 return inverted ? !state : state;
352}
353
354//#########################################################
355// AnalogInput #
356//#########################################################
357
358
360 : pin(sanitizePin(pin)), position(AnalogInput::Min), cal({AnalogInput::Min, AnalogInput::Max})
361{
362 if (pin != UnusedPin) {
363 pinMode(pin, INPUT);
364 }
365}
366
368 bool changed = false;
369
370 if (pin != UnusedPin) {
371 const int previous = this->position;
372 this->position = analogRead(pin);
373
374 // check if value is different for 'changed' flag
375 if (previous != this->position) {
376
377 const int rMin = isInverted() ? getMax() : getMin();
378 const int rMax = isInverted() ? getMin() : getMax();
379
380 if (
381 // if the previous value was under the minimum range
382 // and the current value is as well, no change
383 !(previous < rMin && this->position < rMin) &&
384
385 // if the previous value was over the maximum range
386 // and the current value is as well, no change
387 !(previous > rMax && this->position > rMax)
388 )
389 {
390 // otherwise, the current value is either within the
391 // range limits *or* it has changed from one extreme
392 // to the other. Either way, mark it changed!
393 changed = true;
394 }
395 }
396 }
397 return changed;
398}
399
400long AnalogInput::getPosition(long rMin, long rMax) const {
401 // inversion is handled within the remap function
402 return remap(getPositionRaw(), getMin(), getMax(), rMin, rMax);
403}
404
406 return this->position;
407}
408
410 return (this->cal.min > this->cal.max); // inverted if min is greater than max
411}
412
413void AnalogInput::setPosition(int newPos) {
414 this->position = newPos;
415}
416
417void AnalogInput::setInverted(bool invert) {
418 if (isInverted() == invert) return; // inversion already set
419
420 // to change inversion, swap max and min of the current calibration
421 AnalogInput::Calibration inverted = { this->cal.max, this->cal.min };
422 setCalibration(inverted);
423}
424
426 this->cal = newCal;
427}
428
429//#########################################################
430// Peripheral #
431//#########################################################
432
434 // if the detector exists, poll for state
435 if (this->detector) {
436 this->detector->poll();
437 }
438
439 // get the connected state from the detector
440 const bool connected = this->isConnected();
441
442 // call the derived class update function
443 return this->updateState(connected);
444}
445
447 // if detector exists, return state
448 if (this->detector) {
449 return this->detector->isConnected();
450 }
451
452 // otherwise, assume always connected
453 return true;
454}
455
457 this->detector = d;
458}
459
460void Peripheral::setStablePeriod(unsigned long t) {
461 // if detector exists, set the stable period
462 if (this->detector) {
463 this->detector->setStablePeriod(t);
464 }
465}
466
467//#########################################################
468// Pedals #
469//#########################################################
470
471Pedals::Pedals(AnalogInput* dataPtr, uint8_t nPedals)
472 :
473 pedalData(dataPtr),
474 NumPedals(nPedals),
475 changed(false)
476{}
477
479 update(); // set initial pedal position
480}
481
482bool Pedals::updateState(bool connected) {
483 this->changed = false;
484
485 // if we're connected, read all pedal positions
486 if (connected) {
487 for (int i = 0; i < getNumPedals(); ++i) {
488 changed |= pedalData[i].read();
489 }
490 }
491
492 // otherwise, zero all pedals
493 else {
494 for (int i = 0; i < getNumPedals(); ++i) {
495 const int min = pedalData[i].getMin();
496 const int prev = pedalData[i].getPositionRaw();
497 if (min != prev) {
498 pedalData[i].setPosition(min);
499 changed = true;
500 }
501 }
502 }
503
504 return this->changed;
505}
506
507long Pedals::getPosition(PedalID pedal, long rMin, long rMax) const {
508 if (!hasPedal(pedal)) return rMin; // not a pedal
509 return pedalData[pedal].getPosition(rMin, rMax);
510}
511
513 if (!hasPedal(pedal)) return AnalogInput::Min; // not a pedal
514 return pedalData[pedal].getPositionRaw();
515}
516
517bool Pedals::hasPedal(PedalID pedal) const {
518 return (pedal < getNumPedals());
519}
520
522 if (!hasPedal(pedal)) return;
523 pedalData[pedal].setCalibration(cal);
524 pedalData[pedal].setPosition(pedalData[pedal].getMin()); // reset to min position
525}
526
528 String name;
529
530 switch (pedal) {
531 case(PedalID::Gas):
532 name = F("gas");
533 break;
534 case(PedalID::Brake):
535 name = F("brake");
536 break;
537 case(PedalID::Clutch):
538 name = F("clutch");
539 break;
540 default:
541 name = F("???");
542 break;
543 }
544 return name;
545}
546
547void Pedals::serialCalibration(Stream& iface) {
548 const char* separator = "------------------------------------";
549
550 iface.println();
551 iface.println(F("Sim Racing Library Pedal Calibration"));
552 iface.println(separator);
553 iface.println();
554
555 // read minimums
556 iface.println(F("Take your feet off of the pedals so they move to their resting position."));
557 iface.println(F("Send any character to continue."));
558 waitClient(iface);
559
560 const int MaxPedals = 3; // hard-coded at 3 pedals
561
562 AnalogInput::Calibration pedalCal[MaxPedals];
563
564 // read minimums
565 for (int i = 0; (i < getNumPedals()) && (i < MaxPedals); i++) {
566 pedalData[i].read(); // read position
567 pedalCal[i].min = pedalData[i].getPositionRaw(); // set min to the recorded position
568 }
569 iface.println(F("\nMinimum values for all pedals successfully recorded!\n"));
570 iface.println(separator);
571
572 // read maximums
573 iface.println(F("\nOne at a time, let's measure the maximum range of each pedal.\n"));
574 for (int i = 0; (i < getNumPedals()) && (i < MaxPedals); i++) {
575
576 iface.print(F("Push the "));
577 String name = getPedalName(static_cast<PedalID>(i));
578 name.toLowerCase();
579 iface.print(name);
580 iface.print(F(" pedal to the floor. "));
581
582 iface.println(F("Send any character to continue."));
583 waitClient(iface);
584
585 pedalData[i].read(); // read position
586 pedalCal[i].max = pedalData[i].getPositionRaw(); // set max to the recorded position
587 }
588
589 // deadzone options
590 iface.println(separator);
591 iface.println();
592
593 float DeadzoneMin = 0.01; // by default, 1% (trying to keep things responsive)
594 float DeadzoneMax = 0.025; // by default, 2.5%
595
596 iface.println(F("These settings are optional. Send 'y' to customize. Send any other character to continue with the default values."));
597
598 iface.print(F(" * Pedal Travel Deadzone, Start: \t"));
599 iface.print(DeadzoneMin);
600 iface.println(F(" (Used to avoid the pedal always being slightly pressed)"));
601
602 iface.print(F(" * Pedal Travel Deadzone, End: \t"));
603 iface.print(DeadzoneMax);
604 iface.println(F(" (Used to guarantee that the pedal can be fully pressed)"));
605
606 iface.println();
607
608 waitClient(iface);
609
610 if (iface.read() == 'y') {
611 iface.println(F("Set the pedal travel starting deadzone as a floating point percentage."));
612 readFloat(DeadzoneMin, iface);
613 iface.println();
614
615 iface.println(F("Set the pedal travel ending deadzone as a floating point percentage."));
616 readFloat(DeadzoneMax, iface);
617 iface.println();
618 }
619
620 flushClient(iface);
621
622 // calculate deadzone offsets
623 for (int i = 0; (i < getNumPedals()) && (i < MaxPedals); i++) {
624 auto &cMin = pedalCal[i].min;
625 auto &cMax = pedalCal[i].max;
626
627 const int range = abs(cMax - cMin);
628 const int dzMin = DeadzoneMin * (float)range;
629 const int dzMax = DeadzoneMax * (float)range;
630
631 // non-inverted
632 if (cMax >= cMin) {
633 cMax -= dzMax; // 'cut' into the range so it limits sooner
634 cMin += dzMin;
635 }
636 // inverted
637 else {
638 cMax += dzMax;
639 cMin -= dzMin;
640 }
641 }
642
643 // print finished calibration
644 iface.println(F("Here is your calibration:"));
645 iface.println(separator);
646 iface.println();
647
648 iface.print(F("pedals.setCalibration("));
649
650 for (int i = 0; (i < getNumPedals()) && (i < MaxPedals); i++) {
651 if(i > 0) iface.print(F(", "));
652 iface.print('{');
653
654 iface.print(pedalCal[i].min);
655 iface.print(F(", "));
656 iface.print(pedalCal[i].max);
657
658 iface.print('}');
659
660 this->setCalibration(static_cast<PedalID>(i), pedalCal[i]); // and set it ourselves, too
661 }
662 iface.print(");");
663 iface.println();
664
665 iface.println();
666 iface.println(separator);
667 iface.println();
668
669 iface.print(F("Paste this line into the setup() function. The "));
670 iface.print(F("pedals"));
671 iface.print(F(" will be calibrated with these values on startup."));
672 iface.println(F("\nCalibration complete! :)\n\n"));
673
674 flushClient(iface);
675}
676
677
679 : Pedals(pedalData, NumPedals),
680 pedalData{ AnalogInput(gasPin), AnalogInput(brakePin) }
681{}
682
684 this->Pedals::setCalibration(PedalID::Gas, gasCal);
685 this->Pedals::setCalibration(PedalID::Brake, brakeCal);
686}
687
688
689ThreePedals::ThreePedals(PinNum gasPin, PinNum brakePin, PinNum clutchPin)
690 : Pedals(pedalData, NumPedals),
691 pedalData{ AnalogInput(gasPin), AnalogInput(brakePin), AnalogInput(clutchPin) }
692{}
693
695 this->Pedals::setCalibration(PedalID::Gas, gasCal);
696 this->Pedals::setCalibration(PedalID::Brake, brakeCal);
697 this->Pedals::setCalibration(PedalID::Clutch, clutchCal);
698}
699
700
701LogitechPedals::LogitechPedals(PinNum gasPin, PinNum brakePin, PinNum clutchPin, PinNum detectPin)
702 :
703 ThreePedals(gasPin, brakePin, clutchPin),
704 detectObj(detectPin, false) // active high
705{
706 this->setDetectPtr(&this->detectObj);
707
708 // taken from calibrating my own pedals. the springs are pretty stiff so while
709 // this covers the whole travel range, users may want to back it down for casual
710 // use (esp. for the brake travel)
711 this->setCalibration({ 904, 48 }, { 944, 286 }, { 881, 59 });
712}
713
715 :
716 TwoPedals(gasPin, brakePin),
717 detectObj(detectPin, false) // active high
718{
719 this->setDetectPtr(&this->detectObj);
720 this->setCalibration({ 646, 0 }, { 473, 1023 }); // taken from calibrating my own pedals
721}
722
723
724//#########################################################
725// Shifter #
726//#########################################################
727
729 :
730 MinGear(min), MaxGear(max)
731{
732 this->currentGear = this->previousGear = 0; // neutral
733}
734
736 // if gear is out of range, set it to neutral
737 if (gear < MinGear || gear > MaxGear) {
738 gear = 0;
739 }
740
741 this->previousGear = this->currentGear;
742 this->currentGear = gear;
743}
744
745char Shifter::getGearChar(int gear) {
746 char c = '?';
747
748 switch (gear) {
749 case(-1):
750 c = 'r';
751 break;
752 case(0):
753 c = 'n';
754 break;
755 default:
756 if (gear > 0 && gear <= 9)
757 c = '0' + gear;
758 break;
759 }
760 return c;
761}
762
764 return getGearChar(getGear());
765}
766
767String Shifter::getGearString(int gear) {
768 String name;
769
770 switch (gear) {
771 case(-1):
772 name = F("reverse");
773 break;
774 case(0):
775 name = F("neutral");
776 break;
777 default: {
778 if (gear < 0 || gear > 9) {
779 name = F("???");
780 break; // out of range
781 }
782 name = gear; // set string to current gear
783
784 switch (gear) {
785 case(1):
786 name += F("st");
787 break;
788 case(2):
789 name += F("nd");
790 break;
791 case(3):
792 name += F("rd");
793 break;
794 default:
795 name += F("th");
796 break;
797 }
798 break;
799 }
800 }
801 return name;
802}
803
805 return getGearString(getGear());
806}
807
808
809/* Static calibration constants
810* These values are arbitrary - just what worked well with my own shifter.
811*/
812const float AnalogShifter::CalEngagementPoint = 0.70;
813const float AnalogShifter::CalReleasePoint = 0.50;
814const float AnalogShifter::CalEdgeOffset = 0.60;
815
817 Gear gearMin, Gear gearMax,
818 PinNum pinX, PinNum pinY, PinNum pinRev
819) :
820 Shifter(gearMin, gearMax),
821
822 /* Two axes, X and Y */
823 analogAxis{ AnalogInput(pinX), AnalogInput(pinY) },
824
825 pinReverse(sanitizePin(pinRev)),
826 reverseState(false)
827{}
828
830 if (this->pinReverse != UnusedPin) {
831 pinMode(pinReverse, INPUT);
832 }
833 update(); // set initial gear position
834}
835
836bool AnalogShifter::updateState(bool connected) {
837 // if not connected, reset our position back to neutral
838 // and immediately return
839 if (!connected) {
840 // set axis values to calibrated neutral
841 analogAxis[Axis::X].setPosition(calibration.neutralX);
842 analogAxis[Axis::Y].setPosition(calibration.neutralY);
843
844 // set reverse state to unpressed
845 this->reverseState = false;
846
847 // set gear to neutral
848 this->setGear(0);
849
850 // status changed if gear changed
851 return this->gearChanged();
852 }
853
854 // poll the analog axes for new data
855 analogAxis[Axis::X].read();
856 analogAxis[Axis::Y].read();
857 const int x = analogAxis[Axis::X].getPosition();
858 const int y = analogAxis[Axis::Y].getPosition();
859
860 // poll the reverse button and cache in the class
861 this->reverseState = this->readReverseButton();
862
863 // check previous gears for comparison
864 const Gear previousGear = this->getGear();
865 const bool prevOdd = ((previousGear != -1) && (previousGear & 1)); // were we previously in an odd gear
866 const bool prevEven = (!prevOdd && previousGear != 0); // were we previously in an even gear
867
868 Gear newGear = 0;
869
870 // If we're below the 'release' thresholds, we must still be in the previous gear
871 if ((prevOdd && y > calibration.oddRelease) || (prevEven && y < calibration.evenRelease)) {
872 newGear = previousGear;
873 }
874
875 // If we're *not* below the release thresholds, we may be in a different gear
876 else {
877 // Check if we're in even or odd gears (Y axis)
878 if (y > calibration.oddTrigger) {
879 newGear = 1; // we're in an odd gear
880 }
881 else if (y < calibration.evenTrigger) {
882 newGear = 2; // we're in an even gear
883 }
884
885 if (newGear != 0) {
886 // Now check *which* gear we're in, if we're in one (X axis)
887 if (x > calibration.rightEdge) newGear += 4; // 1-2 + 4 = 5-6
888 else if (x >= calibration.leftEdge) newGear += 2; // 1-2 + 2 = 3-4
889 // (note the '>=', because it would normally be a '<' check for the lower range)
890 // else gear = 1-2 (as set above)
891
892 const bool reverse = getReverseButton();
893
894 // If the reverse button is pressed and we're in 5th gear
895 // something is wrong. Revert that and go back to neutral.
896 if (reverse && newGear == 5) {
897 newGear = 0;
898 }
899
900 // If the reverse button is pressed or we were previously
901 // in reverse *and* we are currently in 6th gear, then we
902 // should be in reverse.
903 else if ((reverse || previousGear == -1) && newGear == 6) {
904 newGear = -1;
905 }
906 }
907 }
908
909 // finally, store the newly calculated gear
910 this->setGear(newGear);
911
912 return this->gearChanged();
913}
914
915long AnalogShifter::getPosition(Axis ax, long min, long max) const {
916 if (ax != Axis::X && ax != Axis::Y) return min; // not an axis
917 return analogAxis[ax].getPosition(min, max);
918}
919
921 if (ax != Axis::X && ax != Axis::Y) return AnalogInput::Min; // not an axis
922 return analogAxis[ax].getPositionRaw();
923}
924
925bool AnalogShifter::readReverseButton() {
926 // if the reverse pin is not set, avoid reading the
927 // floating input and just return 'false'
928 if (pinReverse == UnusedPin) {
929 return false;
930 }
931 return digitalRead(pinReverse);
932}
933
935 // return the cached reverse state from updateState(bool)
936 // do NOT poll the button!
937 return this->reverseState;
938}
939
941 GearPosition neutral,
943 float engagePoint, float releasePoint, float edgeOffset) {
944
945 // limit percentage thresholds
946 engagePoint = floatPercent(engagePoint);
947 releasePoint = floatPercent(releasePoint);
948 edgeOffset = floatPercent(edgeOffset);
949
950 const int xLeft = (g1.x + g2.x) / 2; // find the minimum X position average
951 const int xRight = (g5.x + g6.x) / 2; // find the maximum X position average
952
953 const int yOdd = (g1.y + g3.y + g5.y) / 3; // find the maximum Y position average
954 const int yEven = (g2.y + g4.y + g6.y) / 3; // find the minimum Y position average
955
956 // set X/Y calibration and inversion
957 analogAxis[Axis::X].setCalibration({ xLeft, xRight });
958 analogAxis[Axis::Y].setCalibration({ yEven, yOdd });
959
960 // save neutral values (raw)
961 calibration.neutralX = neutral.x;
962 calibration.neutralY = neutral.y;
963
964 // get normalized and inverted neutral values
965 // this lets us take advantage of the AnalogInput normalization function
966 // that handles inverted axes and automatic range rescaling, so the rest of
967 // the calibration options can be in the normalized range
968 const Axis axes[2] = { Axis::X, Axis::Y };
969 int* const neutralAxis[2] = { &neutral.x, &neutral.y };
970 for (int i = 0; i < 2; i++) {
971 const int previous = analogAxis[axes[i]].getPositionRaw(); // save current value
972 analogAxis[axes[i]].setPosition(*neutralAxis[i]); // set new value to neutral calibration
973 *neutralAxis[i] = analogAxis[axes[i]].getPosition(); // get normalized neutral value
974 analogAxis[axes[i]].setPosition(previous); // reset axis position to previous
975 }
976
977 // calculate the distances between each neutral and the limits of each axis
978 const int yOddDiff = AnalogInput::Max - neutral.y;
979 const int yEvenDiff = neutral.y - AnalogInput::Min;
980
981 const int leftDiff = neutral.x - AnalogInput::Min;
982 const int rightDiff = AnalogInput::Max - neutral.x;
983
984 // calculate and save the trigger and release points for each level
985 calibration.oddTrigger = neutral.y + ((float)yOddDiff * engagePoint);
986 calibration.oddRelease = neutral.y + ((float)yOddDiff * releasePoint);
987
988 calibration.evenTrigger = neutral.y - ((float)yEvenDiff * engagePoint);
989 calibration.evenRelease = neutral.y - ((float)yEvenDiff * releasePoint);
990
991 calibration.leftEdge = neutral.x - ((float)leftDiff * edgeOffset);
992 calibration.rightEdge = neutral.x + ((float)rightDiff * edgeOffset);
993
994#if 0
995 Serial.print("Odd Trigger: ");
996 Serial.println(calibration.oddTrigger);
997 Serial.print("Odd Release: ");
998 Serial.println(calibration.oddRelease);
999 Serial.print("Even Trigger: ");
1000 Serial.println(calibration.evenTrigger);
1001 Serial.print("Even Release: ");
1002 Serial.println(calibration.evenRelease);
1003 Serial.print("Left Edge: ");
1004 Serial.println(calibration.leftEdge);
1005 Serial.print("Right Edge: ");
1006 Serial.println(calibration.rightEdge);
1007 Serial.println();
1008
1009 Serial.print("X Min: ");
1010 Serial.println(analogAxis[Axis::X].getMin());
1011 Serial.print("X Max: ");
1012 Serial.println(analogAxis[Axis::X].getMax());
1013
1014 Serial.print("Y Min: ");
1015 Serial.println(analogAxis[Axis::Y].getMin());
1016 Serial.print("Y Max: ");
1017 Serial.println(analogAxis[Axis::Y].getMax());
1018#endif
1019}
1020
1022 if (isConnected() == false) {
1023 iface.print(F("Error! Cannot perform calibration, "));
1024 iface.print(F("shifter"));
1025 iface.println(F(" is not connected."));
1026 return;
1027 }
1028
1029 const char* separator = "------------------------------------";
1030
1031 iface.println();
1032 iface.println(F("Sim Racing Library Shifter Calibration"));
1033 iface.println(separator);
1034 iface.println();
1035
1036 AnalogShifter::GearPosition gears[7]; // neutral, then 1-6
1037 float engagementPoint = CalEngagementPoint;
1038 float releasePoint = CalReleasePoint;
1039 float edgeOffset = CalEdgeOffset;
1040
1041 for (int i = 0; i <= 6; i++) {
1042 const String gearName = this->getGearString(i);
1043
1044 iface.print(F("Please move the gear shifter into "));
1045 iface.print(gearName);
1046 iface.println(F(". Send any character to continue."));
1047
1048 waitClient(iface);
1049
1050 this->update();
1051 gears[i] = {
1052 this->analogAxis[Axis::X].getPositionRaw(),
1053 this->analogAxis[Axis::Y].getPositionRaw()
1054 };
1055
1056 iface.print("Gear '");
1057 iface.print(gearName);
1058 iface.print("' position recorded as { ");
1059 iface.print(gears[i].x);
1060 iface.print(", ");
1061 iface.print(gears[i].y);
1062 iface.println(" }");
1063 iface.println();
1064 }
1065
1066 iface.println(separator);
1067 iface.println();
1068
1069 iface.println(F("These settings are optional. Send 'y' to customize. Send any other character to continue with the default values."));
1070
1071 iface.print(F(" * Gear Engagement Point: \t"));
1072 iface.println(engagementPoint);
1073
1074 iface.print(F(" * Gear Release Point: \t"));
1075 iface.println(releasePoint);
1076
1077 iface.print(F(" * Horizontal Gate Offset:\t"));
1078 iface.println(edgeOffset);
1079
1080 iface.println();
1081
1082 waitClient(iface);
1083
1084 if (iface.read() == 'y') {
1085 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."));
1086 readFloat(engagementPoint, iface);
1087 iface.println();
1088
1089 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."));
1090 readFloat(releasePoint, iface);
1091 iface.println();
1092
1093 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."));
1094 readFloat(edgeOffset, iface);
1095 iface.println();
1096 }
1097
1098 flushClient(iface);
1099
1100 this->setCalibration(gears[0], gears[1], gears[2], gears[3], gears[4], gears[5], gears[6], engagementPoint, releasePoint, edgeOffset);
1101
1102 iface.println(F("Here is your calibration:"));
1103 iface.println(separator);
1104 iface.println();
1105
1106 iface.print(F("shifter.setCalibration( "));
1107
1108 for (int i = 0; i < 7; i++) {
1109 iface.print('{');
1110
1111 iface.print(gears[i].x);
1112 iface.print(", ");
1113 iface.print(gears[i].y);
1114
1115 iface.print('}');
1116 iface.print(", ");
1117 }
1118 iface.print(engagementPoint);
1119 iface.print(", ");
1120 iface.print(releasePoint);
1121 iface.print(", ");
1122 iface.print(edgeOffset);
1123 iface.print(");");
1124 iface.println();
1125
1126 iface.println();
1127 iface.println(separator);
1128 iface.println();
1129
1130 iface.println(F("Paste this line into the setup() function to calibrate on startup."));
1131 iface.println(F("\n\nCalibration complete! :)\n"));
1132}
1133
1135 :
1137 -1, 6, // includes reverse and gears 1-6
1138 pinX, pinY, pinRev
1139 ),
1140
1141 detectObj(detectPin, false) // active high
1142{
1143 this->setDetectPtr(&this->detectObj);
1144 this->setCalibration({ 490, 440 }, { 253, 799 }, { 262, 86 }, { 460, 826 }, { 470, 76 }, { 664, 841 }, { 677, 77 });
1145}
1146
1147
1149 PinNum pinX, PinNum pinY,
1150 PinNum pinLatch, PinNum pinClock, PinNum pinData,
1151 PinNum pinLed,
1152 PinNum pinDetect
1153) :
1154 LogitechShifter(pinX, pinY, UnusedPin, pinDetect),
1155
1156 pinLatch(sanitizePin(pinLatch)), pinClock(sanitizePin(pinClock)), pinData(sanitizePin(pinData)),
1157 pinLed(sanitizePin(pinLed))
1158{
1159 this->pinModesSet = false;
1160 this->setPowerLED(1); // power LED on by default
1161 this->buttonStates = this->previousButtons = 0x0000; // zero all button data
1162
1163 // using the calibration values from my own G27 shifter
1164 this->setCalibration({ 453, 470 }, { 247, 828 }, { 258, 6 }, { 449, 878 }, { 472, 5 }, { 645, 880 }, { 651, 21 });
1165}
1166
1167void LogitechShifterG27::cacheButtons(uint16_t newStates) {
1168 this->previousButtons = this->buttonStates; // save current to previous
1169 this->buttonStates = newStates; // replace current with new value
1170}
1171
1172void LogitechShifterG27::setPinModes(bool enabled) {
1173 // check if pins are valid. if one or more pins is unused,
1174 // this isn't going to work and we shouldn't bother setting
1175 // any of the pin states
1176 if (
1177 this->pinData == UnusedPin ||
1178 this->pinLatch == UnusedPin ||
1179 this->pinClock == UnusedPin)
1180 {
1181 return;
1182 }
1183
1184 // set up data pin to read from regardless
1185 pinMode(this->pinData, INPUT);
1186
1187 // enabled = drive the output pins
1188 if (enabled) {
1189 // note: writing the output before setting the
1190 // pin mode so that we don't accidentally drive
1191 // the wrong direction momentarily
1192
1193 // set latch pin as output, HIGH on idle
1194 digitalWrite(this->pinLatch, HIGH);
1195 pinMode(this->pinLatch, OUTPUT);
1196
1197 // set clock pin as output, LOW on idle
1198 digitalWrite(this->pinClock, LOW);
1199 pinMode(this->pinClock, OUTPUT);
1200
1201 // if we have an LED pin, set it to output and write the
1202 // commanded state (inverted, as the LED is active-low)
1203 if (this->pinLed != UnusedPin) {
1204 digitalWrite(this->pinLed, !(this->ledState));
1205 pinMode(this->pinLed, OUTPUT);
1206 }
1207 }
1208
1209 // disabled = leave output pins as high-z
1210 else {
1211 // note: setting the mode before writing the
1212 // output for the same reason; changing in
1213 // high-z mode is safer
1214
1215 // set latch pin as high impedance, with pull-up
1216 pinMode(this->pinLatch, INPUT);
1217 digitalWrite(this->pinLatch, HIGH);
1218
1219 // set clock pin as high impedance, no pull-up
1220 pinMode(this->pinClock, INPUT);
1221 digitalWrite(this->pinClock, LOW);
1222
1223 // if we have an LED pin, set it to input, LOW on idle
1224 if (this->pinLed != UnusedPin) {
1225 pinMode(this->pinLed, INPUT);
1226 digitalWrite(this->pinLed, LOW);
1227 }
1228 }
1229
1230 this->pinModesSet = enabled;
1231}
1232
1234 this->ledState = state;
1235}
1236
1237uint16_t LogitechShifterG27::readShiftRegisters() {
1238 // if the pin outputs are not set, quit (none pressed)
1239 if (!this->pinModesSet) return 0x0000;
1240
1241 uint16_t data = 0x0000;
1242
1243 // pulse shift register latch from high to low to high, 12 us
1244 // (this timing is *completely* arbitrary, but it's nice to have
1245 // *some* delay so that much faster MCUs don't blow through it)
1246 digitalWrite(this->pinLatch, LOW);
1247 delayMicroseconds(12);
1248 digitalWrite(this->pinLatch, HIGH);
1249 delayMicroseconds(12);
1250
1251 // clock is pulsed from LOW to HIGH on every bit,
1252 // and then left to idle low
1253 for (int i = 0; i < 16; ++i) {
1254 digitalWrite(this->pinClock, LOW);
1255 const bool state = digitalRead(this->pinData);
1256 if (state) data |= 1 << (15 - i); // store data in word, MSB-first
1257 digitalWrite(this->pinClock, HIGH);
1258 delayMicroseconds(6);
1259 }
1260 digitalWrite(this->pinClock, LOW);
1261
1262 // edge case: two of the bits (0x8000 and 0x2000) are connected only to
1263 // pull-down resistors, and should theoretically never be high. If they,
1264 // and all other bits, *are* high, then we are not reading from a shifter
1265 // that has shift registers. The "Driving Force" (G29/G920/G923) shifter
1266 // has its data output connected to the 'reverse' button through a buffer,
1267 // and will report 'high' if the reverse button is pressed no matter how
1268 // many times the clock is pulsed.
1269 //
1270 // QED: we are connected to a "Driving Force" shifter, and not a G27.
1271 // That's okay! If we set the state of the 'reverse' button and clear
1272 // all others, we can still behave like a G27.
1273 if (data == 0xFFFF) {
1274 data = (1 << (uint8_t) Button::BUTTON_REVERSE);
1275 }
1276
1277 return data;
1278}
1279
1281 // disable pin outputs. this sets the initial
1282 // 'safe' state. the outputs will be enabled
1283 // by the 'updateState(bool)' function when needed.
1284 this->setPinModes(0);
1285
1286 // call the begin() class of the base, which will also
1287 // poll 'update()' on our behalf
1288 this->AnalogShifter::begin();
1289}
1290
1292 bool changed = false;
1293
1294 // if we're connected, set the pin modes, read the
1295 // shift registers, and cache the data
1296 if (connected) {
1297 if (!this->pinModesSet) {
1298 this->setPinModes(1);
1299 }
1300
1301 if (this->pinLed != UnusedPin) {
1302 digitalWrite(this->pinLed, !(this->ledState)); // active low
1303 }
1304
1305 const uint16_t data = this->readShiftRegisters();
1306 this->cacheButtons(data);
1307 changed |= this->buttonsChanged();
1308 }
1309
1310 // if we're *not* connected, reset the pin modes and
1311 // set no buttons pressed
1312 else {
1313 if (this->pinModesSet) {
1314 this->setPinModes(0);
1315 }
1316
1317 this->cacheButtons(0x0000);
1318 changed |= this->buttonsChanged();
1319 }
1320
1321 // we also need to update the data for the analog shifter
1322 changed |= AnalogShifter::updateState(connected);
1323
1324 return changed;
1325}
1326
1328 return this->buttonStates != this->previousButtons;
1329}
1330
1332 return this->extractButton(button, this->buttonStates);
1333}
1334
1336 return this->getButton(button) != this->extractButton(button, this->previousButtons);
1337}
1338
1340 const Button pads[4] = {
1341 DPAD_UP,
1342 DPAD_RIGHT,
1343 DPAD_DOWN,
1344 DPAD_LEFT,
1345 };
1346
1347 // combine pads to a bitfield (nybble)
1348 uint8_t dpad = 0x00;
1349 for (uint8_t i = 0; i < 4; ++i) {
1350 dpad |= (this->getButton(pads[i]) << i);
1351 }
1352
1353 // The hatswitch value is from 0-7 proceeding clockwise
1354 // from top (0 is 'up', 1 is 'up + right', etc.). I don't
1355 // know of a great way to do this, so have this naive
1356 // lookup table with a built-in SOCD cleaner
1357
1358 // For this, simultaneous opposing cardinal directions
1359 // are neutral (because this is presumably used for
1360 // navigation only, and not fighting games. Probably).
1361
1362 // bitfield to hatswitch lookup table
1363 const uint8_t hat_table[16] = {
1364 8, // 0b0000, Unpressed
1365 0, // 0b0001, Up
1366 2, // 0b0010, Right
1367 1, // 0b0011, Right + Up
1368 4, // 0b0100, Down
1369 8, // 0b0101, Down + Up (SOCD None)
1370 3, // 0b0110, Down + Right
1371 2, // 0b0111, Down + Right + Up (SOCD Right)
1372 6, // 0b1000, Left
1373 7, // 0b1001, Left + Up
1374 8, // 0b1010, Left + Right (SOCD None)
1375 0, // 0b1011, Left + Right + Up (SOCD Up)
1376 5, // 0b1100, Left + Down
1377 6, // 0b1101, Left + Down + Up (SOCD Left)
1378 4, // 0b1110, Left + Down + Right (SOCD Down)
1379 8, // 0b1111, Left + Down + Right + Up (SOCD None)
1380 };
1381
1382 // multiply the 0-8 value by 45 to get it in degrees
1383 int16_t angle = hat_table[dpad & 0x0F] * 45;
1384
1385 // edge case: if no buttons are pressed, the angle is '-1'
1386 if (angle == 360) angle = -1;
1387
1388 return angle;
1389}
1390
1391bool LogitechShifterG27::readReverseButton() {
1392 // this virtual function is provided for the sake of the AnalogShifter base
1393 // class, which can use this to get the button state from the shift register
1394 // without needing to interface with the shift registers themselves
1395 return this->getButton(BUTTON_REVERSE);
1396}
1397
1398
1399/*
1400* Static calibration constants
1401* These values are arbitrary - just what worked well with my own shifter.
1402*/
1403const float LogitechShifterG25::CalEngagementPoint = 0.70;
1404const float LogitechShifterG25::CalReleasePoint = 0.50;
1405
1407 PinNum pinX, PinNum pinY,
1408 PinNum pinLatch, PinNum pinClock, PinNum pinData,
1409 PinNum pinLed,
1410 PinNum pinDetect
1411) :
1413 pinX, pinY,
1414 pinLatch, pinClock, pinData,
1415 pinLed,
1416 pinDetect
1417 ),
1418
1419 sequentialProcess(false), // not in sequential mode
1420 sequentialState(0) // no sequential buttons pressed
1421{
1422 // using the calibration values from my own G25 shifter
1423 this->setCalibration({ 508, 435 }, { 310, 843 }, { 303, 8 }, { 516, 827 }, { 540, 14 }, { 713, 846 }, { 704, 17 });
1424 this->setCalibrationSequential(425, 619, 257);
1425}
1426
1428 this->sequentialProcess = false; // clear process flag
1429 this->sequentialState = 0; // clear any pressed buttons
1430
1431 this->LogitechShifterG27::begin(); // call base class begin()
1432}
1433
1435 // call the base class to update the state of the
1436 // buttons and the H-pattern shifter
1437 bool changed = this->LogitechShifterG27::updateState(connected);
1438
1439 // if we're connected and in sequential mode...
1440 if (connected && this->inSequentialMode()) {
1441
1442 // clear 'changed', because this will falsely report a change
1443 // if we've "shifted" into 2nd/4th in the process of sequential
1444 // shifting
1445 changed = false;
1446
1447 // force neutral gear, ignoring the H-pattern selection
1448 this->setGear(0);
1449
1450 // edge case: if we've not just switched into sequential mode,
1451 // we need to ignore the H-pattern gear change (to 2/4, and then
1452 // set by us to neutral). We can do that, hackily, by setting to
1453 // neutral again to clear the cached gear for comparison.
1454 if (this->sequentialProcess) {
1455 this->setGear(0);
1456 }
1457
1458 // read the raw y axis value, ignoring the H-pattern calibration
1459 const int y = this->getPositionRaw(Axis::Y);
1460
1461 // save the previous state for reference
1462 const int8_t prevState = this->sequentialState;
1463
1464 // if we're neutral, check for up/down shift
1465 if (this->sequentialState == 0) {
1466 if (y >= this->seqCalibration.upTrigger) this->sequentialState = 1;
1467 else if (y <= this->seqCalibration.downTrigger) this->sequentialState = -1;
1468 }
1469
1470 // if we're in up-shift mode, check for release
1471 else if ((this->sequentialState == 1) && (y < this->seqCalibration.upRelease)) {
1472 this->sequentialState = 0;
1473 }
1474
1475 // if we're in down-shift mode, check for release
1476 else if ((this->sequentialState == -1) && (y > this->seqCalibration.downRelease)) {
1477 this->sequentialState = 0;
1478 }
1479
1480 // set the 'changed' flag if the sequential state changed
1481 if (prevState != this->sequentialState) {
1482 changed = true;
1483 }
1484 // otherwise, set 'changed' based on the buttons *only*
1485 else {
1486 changed = this->buttonsChanged();
1487 }
1488
1489 // set 'process' flag to handle edge case on subsequent updates
1490 this->sequentialProcess = true;
1491 }
1492
1493 // if we're not connected or if the sequential mode has been disabled,
1494 // clear the sequential flags if they have been set
1495 else {
1496 if (this->sequentialProcess) {
1497 this->sequentialProcess = false; // not in sequential mode
1498 this->sequentialState = 0; // no sequential buttons pressed
1499 changed = true;
1500 }
1501 }
1502
1503 return changed;
1504}
1505
1509
1511 return this->sequentialState == 1;
1512}
1513
1515 return this->sequentialState == -1;
1516}
1517
1518void LogitechShifterG25::setCalibrationSequential(int neutral, int up, int down, float engagePoint, float releasePoint) {
1519 // limit percentage thresholds
1520 engagePoint = floatPercent(engagePoint);
1521 releasePoint = floatPercent(releasePoint);
1522
1523 // prevent release point from being higher than engage
1524 // (which will prevent the shifter from working at all)
1525 if (releasePoint > engagePoint) {
1526 releasePoint = engagePoint;
1527 }
1528
1529 // calculate ranges
1530 const int upRange = up - neutral;
1531 const int downRange = neutral - down;
1532
1533 // calculate calibration points
1534 this->seqCalibration.upTrigger = neutral + (upRange * engagePoint);
1535 this->seqCalibration.upRelease = neutral + (upRange * releasePoint);
1536
1537 this->seqCalibration.downTrigger = neutral - (downRange * engagePoint);
1538 this->seqCalibration.downRelease = neutral - (downRange * releasePoint);
1539}
1540
1542 // err if not connected
1543 if (this->isConnected() == false) {
1544 iface.print(F("Error! Cannot perform calibration, "));
1545 iface.print(F("shifter"));
1546 iface.println(F(" is not connected."));
1547 return;
1548 }
1549
1550 const char* separator = "------------------------------------";
1551
1552 iface.println();
1553 iface.println(F("Sim Racing Library G25 Sequential Shifter Calibration"));
1554 iface.println(separator);
1555 iface.println();
1556
1557 while (this->inSequentialMode() == false) {
1558 iface.print(F("Please press down on the shifter and move the dial counter-clockwise to put the shifter into sequential mode"));
1559 iface.print(F(". Send any character to continue."));
1560 iface.println(F(" Send 'q' to quit."));
1561 iface.println();
1562
1563 waitClient(iface);
1564 this->update();
1565
1566 // quit if user sends 'q'
1567 if (iface.read() == 'q') {
1568 iface.println(F("Quitting sequential calibration! Goodbye <3"));
1569 iface.println();
1570 return;
1571 }
1572
1573 // send an error if we're still not there
1574 if (this->inSequentialMode() == false) {
1575 iface.println(F("Error: The shifter is not in sequential mode"));
1576 iface.println();
1577 }
1578 }
1579
1580 float engagementPoint = LogitechShifterG25::CalEngagementPoint;
1581 float releasePoint = LogitechShifterG25::CalReleasePoint;
1582
1583 const uint8_t NumPoints = 3;
1584 const char* directions[2] = {
1585 "up",
1586 "down",
1587 };
1588 int data[NumPoints];
1589
1590 int& neutral = data[0];
1591 int& yMax = data[1];
1592 int& yMin = data[2];
1593
1594 for (uint8_t i = 0; i < NumPoints; ++i) {
1595 if (i == 0) {
1596 iface.print(F("Leave the gear shifter in neutral"));
1597 }
1598 else {
1599 iface.print(F("Please move the gear shifter to sequentially shift "));
1600 iface.print(directions[i - 1]);
1601 iface.print(F(" and hold it there"));
1602 }
1603 iface.println(F(". Send any character to continue."));
1604 waitClient(iface);
1605
1606 this->update();
1607 data[i] = this->getPositionRaw(Axis::Y);
1608 iface.println(); // spacing
1609 }
1610
1611 iface.println(F("These settings are optional. Send 'y' to customize. Send any other character to continue with the default values."));
1612
1613 iface.print(F(" * Shift Engagement Point: \t"));
1614 iface.println(engagementPoint);
1615
1616 iface.print(F(" * Shift Release Point: \t"));
1617 iface.println(releasePoint);
1618
1619 iface.println();
1620
1621 waitClient(iface);
1622
1623 if (iface.read() == 'y') {
1624 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."));
1625 readFloat(engagementPoint, iface);
1626 iface.println();
1627
1628 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."));
1629 readFloat(releasePoint, iface);
1630 iface.println();
1631 }
1632
1633 flushClient(iface);
1634
1635 // apply and print
1636 this->setCalibrationSequential(neutral, yMax, yMin, engagementPoint, releasePoint);
1637
1638 iface.println(F("Here is your calibration:"));
1639 iface.println(separator);
1640 iface.println();
1641
1642 iface.print(F("shifter.setCalibrationSequential( "));
1643
1644 iface.print(neutral);
1645 iface.print(", ");
1646 iface.print(yMax);
1647 iface.print(", ");
1648 iface.print(yMin);
1649 iface.print(", ");
1650
1651 iface.print(engagementPoint);
1652 iface.print(", ");
1653 iface.print(releasePoint);
1654 iface.print(");");
1655 iface.println();
1656
1657 iface.println();
1658 iface.println(separator);
1659 iface.println();
1660
1661 iface.println(F("Paste this line into the setup() function to calibrate on startup."));
1662 iface.println(F("\n\nCalibration complete! :)\n"));
1663}
1664
1665//#########################################################
1666// Handbrake #
1667//#########################################################
1668
1670 :
1671 analogAxis(pinAx),
1672 changed(false)
1673{}
1674
1676 update(); // set initial handbrake position
1677}
1678
1679bool Handbrake::updateState(bool connected) {
1680 this->changed = false;
1681
1682 // if connected, read state of the axis
1683 if (connected) {
1684 this->changed = this->analogAxis.read();
1685 }
1686
1687 // otherwise, set axis to its minimum (idle) position
1688 else {
1689 const int min = this->analogAxis.getMin();
1690 const int prev = this->analogAxis.getPositionRaw();
1691
1692 if (min != prev) {
1693 this->analogAxis.setPosition(min);
1694 this->changed = true;
1695 }
1696 }
1697
1698 return this->changed;
1699}
1700
1701long Handbrake::getPosition(long rMin, long rMax) const {
1702 return analogAxis.getPosition(rMin, rMax);
1703}
1704
1706 return analogAxis.getPositionRaw();
1707}
1708
1710 analogAxis.setCalibration(newCal);
1711 analogAxis.setPosition(analogAxis.getMin()); // reset to min
1712}
1713
1714void Handbrake::serialCalibration(Stream& iface) {
1715 if (isConnected() == false) {
1716 iface.print(F("Error! Cannot perform calibration, "));
1717 iface.print(F("handbrake"));
1718 iface.println(F(" is not connected."));
1719 return;
1720 }
1721
1722 const char* separator = "------------------------------------";
1723
1724 iface.println();
1725 iface.println(F("Sim Racing Library Handbrake Calibration"));
1726 iface.println(separator);
1727 iface.println();
1728
1730
1731 // read minimum
1732 iface.println(F("Keep your hand off of the handbrake to record its resting position"));
1733 iface.println(F("Send any character to continue."));
1734 waitClient(iface);
1735
1736 analogAxis.read();
1737 newCal.min = analogAxis.getPositionRaw();
1738 iface.println();
1739
1740 // read maximum
1741 iface.println(F("Now pull on the handbrake and hold it at the end of its range"));
1742 iface.println(F("Send any character to continue."));
1743 waitClient(iface);
1744
1745 analogAxis.read();
1746 newCal.max = analogAxis.getPositionRaw();
1747 iface.println();
1748
1749 // set new calibration
1750 this->setCalibration(newCal);
1751
1752 // print finished calibration
1753 iface.println(F("Here is your calibration:"));
1754 iface.println(separator);
1755 iface.println();
1756
1757 iface.print(F("handbrake.setCalibration("));
1758 iface.print('{');
1759 iface.print(newCal.min);
1760 iface.print(F(", "));
1761 iface.print(newCal.max);
1762 iface.print("});");
1763 iface.println();
1764
1765 iface.println();
1766 iface.println(separator);
1767 iface.println();
1768
1769 iface.print(F("Paste this line into the setup() function. The "));
1770 iface.print(F("handbrake"));
1771 iface.print(F(" will be calibrated with these values on startup."));
1772 iface.println(F("\nCalibration complete! :)\n\n"));
1773
1774 flushClient(iface);
1775}
1776
1777}; // 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:138
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:141
int getMax() const
Retrieves the calibrated maximum position.
Definition SimRacing.h:190
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:183
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:140
AnalogInput(PinNum pin)
Class constructor.
Interface with shifters using two potentiometers for gear position.
Definition SimRacing.h:614
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(Gear gearMin, Gear gearMax, PinNum pinX, PinNum pinY, PinNum pinRev=UnusedPin)
Class constructor.
int getPositionRaw(Axis ax) const
Retrieves the buffered position for the analog axis.
virtual bool updateState(bool connected)
Perform an internal poll of the hardware to refresh the class state.
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...
Used for tracking whether a device is connected to a specific pin and stable.
Definition SimRacing.h:59
void poll()
Checks if the pin detects a connection.
void setStablePeriod(unsigned long t)
Set how long the detection pin must be stable for before the device is considered to be 'connected'.
ConnectionState getState() const
Retrieves the current ConnectionState from the instance.
DeviceConnection(PinNum pin, bool activeLow=false, unsigned long detectTime=250)
Class constructor.
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
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.
Handbrake(PinNum pinAx)
Class constructor.
virtual bool updateState(bool connected)
Perform an internal poll of the hardware to refresh the class state.
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.
Interface with the Logitech pedals (Gas, Brake, and Clutch)
Definition SimRacing.h:835
LogitechPedals(PinNum pinGas, PinNum pinBrake, PinNum pinClutch, PinNum pinDetect=UnusedPin)
Class constructor.
Interface with the Logitech G25 shifter.
Definition SimRacing.h:1123
bool getShiftDown() const
Check if the sequential shifter is shifted down.
LogitechShifterG25(PinNum pinX, PinNum pinY, PinNum pinLatch, PinNum pinClock, PinNum pinData, PinNum pinLed=UnusedPin, PinNum pinDetect=UnusedPin)
Class constructor.
void serialCalibrationSequential(Stream &iface=Serial)
Runs an interactive calibration tool using the serial interface.
virtual bool updateState(bool connected)
Perform an internal poll of the hardware to refresh the class state.
bool getShiftUp() const
Check if the sequential shifter is shifted up.
bool inSequentialMode() const
Check if the shifter is in sequential shifting mode.
void setCalibrationSequential(int neutral, int up, int down, float engagePoint=LogitechShifterG25::CalEngagementPoint, float releasePoint=LogitechShifterG25::CalReleasePoint)
Calibrate the sequential shifter for more accurate shifting.
virtual void begin()
Initializes the hardware pins for reading the gear states and the buttons.
Interface with the Logitech G27 shifter.
Definition SimRacing.h:937
int getDpadAngle() const
Get the directional pad angle in degrees.
Button
Enumeration of button values.
Definition SimRacing.h:949
@ BUTTON_REVERSE
Reverse button (press down on the shifter)
Definition SimRacing.h:951
@ BUTTON_SEQUENTIAL
Sequential mode button (turn the dial counter-clockwise)
Definition SimRacing.h:953
@ DPAD_DOWN
Down button of the directional pad.
Definition SimRacing.h:964
@ DPAD_RIGHT
Right button of the directional pad.
Definition SimRacing.h:962
@ DPAD_UP
Top button of the directional pad.
Definition SimRacing.h:965
@ DPAD_LEFT
Left button of the directional pad.
Definition SimRacing.h:963
bool getButton(Button button) const
Retrieve the state of a single button.
bool buttonsChanged() const
Checks if any of the buttons have changed since the last update.
LogitechShifterG27(PinNum pinX, PinNum pinY, PinNum pinLatch, PinNum pinClock, PinNum pinData, PinNum pinLed=UnusedPin, PinNum pinDetect=UnusedPin)
Class constructor.
bool getButtonChanged(Button button) const
Checks whether a button has changed between updates.
virtual void begin()
Initializes the hardware pins for reading the gear states and the buttons.
virtual bool updateState(bool connected)
Perform an internal poll of the hardware to refresh the class state.
void setPowerLED(bool state)
Sets the state of the shifter's power LED.
Interface with the Logitech Driving Force shifter.
Definition SimRacing.h:883
LogitechShifter(PinNum pinX, PinNum pinY, PinNum pinRev=UnusedPin, PinNum pinDetect=UnusedPin)
Class constructor.
Base class for all pedals instances.
Definition SimRacing.h:335
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)
Class constructor.
static String getPedalName(PedalID pedal)
Utility function to get the string name for each pedal.
virtual bool updateState(bool connected)
Perform an internal poll of the hardware to refresh the class state.
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 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:390
bool isConnected() const
Check if the device is physically connected to the board.
void setDetectPtr(DeviceConnection *d)
Sets the pointer to the detector object.
bool update()
Perform a poll of the hardware to refresh the class state.
virtual bool updateState(bool connected)=0
Perform an internal poll of the hardware to refresh the class state.
void setStablePeriod(unsigned long t)
Set how long the detection pin must be stable for before the device is considered to be 'connected'.
Base class for all shifter instances.
Definition SimRacing.h:505
String getGearString() const
Returns a String that represents the current gear.
void setGear(Gear gear)
Changes the currently set gear, internally.
Shifter(Gear min, Gear max)
Class constructor.
int8_t Gear
Type alias for gear numbers.
Definition SimRacing.h:510
char getGearChar() const
Returns a character that represents the current gear.
Gear getGear() const
Returns the currently selected gear.
Definition SimRacing.h:528
bool gearChanged() const
Checks whether the current gear has changed since the last update.
Definition SimRacing.h:572
Pedal implementation for devices with gas, brake, and clutch.
Definition SimRacing.h:466
ThreePedals(PinNum pinGas, PinNum pinBrake, PinNum pinClutch)
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:437
void setCalibration(AnalogInput::Calibration gasCal, AnalogInput::Calibration brakeCal)
Sets the calibration data (min/max) for the pedals.
TwoPedals(PinNum pinGas, PinNum pinBrake)
Class constructor.
Pedal
Pedal ID names.
Definition SimRacing.h:324
Simple struct containing min/max values for axis calibration.
Definition SimRacing.h:229
int max
Maximum value of the analog axis.
Definition SimRacing.h:231
int min
Minimum value of the analog axis.
Definition SimRacing.h:230
Simple struct to store X/Y coordinates for the calibration function.
Definition SimRacing.h:666
int y
Y coordinate of the gear position from the ADC.
Definition SimRacing.h:668
int x
X coordinate of the gear position from the ADC.
Definition SimRacing.h:667