diff --git a/README.md b/README.md index f6933ef..87657da 100644 --- a/README.md +++ b/README.md @@ -23,11 +23,13 @@ Run one of the library examples in the Arduino IDE by going to `File -> Examples ### Commercial Devices * [Logitech Two Pedal Peripheral (Gas, Brake)](http://dmadison.github.io/Sim-Racing-Arduino/docs/logitech_pedals.html) * [Logitech Three Pedal Peripheral (Gas, Brake, Clutch)](http://dmadison.github.io/Sim-Racing-Arduino/docs/logitech_pedals.html) -* [Logitech Driving Force Shifter](http://dmadison.github.io/Sim-Racing-Arduino/docs/logitech_shifter.html) +* [Logitech Driving Force Shifter (G923 / G920 / G29)](http://dmadison.github.io/Sim-Racing-Arduino/docs/logitech_shifter.html) +* [Logitech G27 Shifter](http://dmadison.github.io/Sim-Racing-Arduino/docs/logitech_shifter_g27.html) +* [Logitech G25 Shifter](http://dmadison.github.io/Sim-Racing-Arduino/docs/logitech_shifter_g25.html) ## Adapters -Open source shields are available to connect the [Logitech Three Pedal Peripheral](http://dmadison.github.io/Sim-Racing-Arduino/docs/logitech_pedals.html) and the [Logitech Driving Force Shifter](http://dmadison.github.io/Sim-Racing-Arduino/docs/logitech_shifter.html) to a [SparkFun Pro Micro](https://github.com/sparkfun/Pro_Micro). The design comes with a 3D printable case and custom board files so that the adapter appears with a custom identity and "Sim Racing" name over USB. You can use these shields to build an inexpensive USB HID adapter. +Open source shields are available to connect the [Logitech Three Pedal Peripheral](http://dmadison.github.io/Sim-Racing-Arduino/docs/logitech_pedals.html) and the Logitech shifters ([Driving Force](http://dmadison.github.io/Sim-Racing-Arduino/docs/logitech_shifter.html) / [G27](http://dmadison.github.io/Sim-Racing-Arduino/docs/logitech_shifter_g27.html) / [G25](http://dmadison.github.io/Sim-Racing-Arduino/docs/logitech_shifter_g25.html)) to a [SparkFun Pro Micro](https://github.com/sparkfun/Pro_Micro). The design comes with a 3D printable case and custom board files so that the adapter appears with a custom identity and "Sim Racing" name over USB. You can use these shields to build an inexpensive USB HID adapter. You can find all of the necessary files in [the project repository](https://github.com/dmadison/Sim-Racing-Shields). diff --git a/docs/pages/devices/logitech_shifter.md b/docs/pages/devices/logitech_shifter.md index 90ed00f..7509da5 100644 --- a/docs/pages/devices/logitech_shifter.md +++ b/docs/pages/devices/logitech_shifter.md @@ -1,14 +1,14 @@ -# Logitech Driving Force Shifter {#logitech_shifter} +# Logitech Driving Force Shifter (G923 / G920 / G29) {#logitech_shifter} -The [Logitech Driving Force Shifter](https://www.logitechg.com/en-us/products/driving/driving-force-shifter.html) is implemented using the SimRacing::LogitechShifter class. +The [Logitech Driving Force Shifter](https://www.logitechg.com/en-us/products/driving/driving-force-shifter.html) is implemented using the SimRacing::LogitechShifter class. This shifter is included with the [G923](https://www.logitechg.com/en-us/products/driving/g923-trueforce-sim-racing-wheel.html), [G920](https://en.wikipedia.org/wiki/Logitech_G29), and [G29](https://en.wikipedia.org/wiki/Logitech_G29) wheels. -See the ShiftPrint.ino and ShiftJoystick.ino examples for reference. +See the LogitechShifter_Print.ino and LogitechShifter_Joystick.ino examples for reference. ## Adapters -@youtube_embed{https://www.youtube.com/embed/ngXsOidoWhI} +@youtube_embed{https://www.youtube.com/embed/yLL9XBgx8bs} -The best way to connect to the shifter is to build your own DIY adapter using a male DE-9 connector. This is simple to make and does not require any modifications to the shifter. The above video walks you through the process of wiring to an Arduino Leonardo. +You can build your own DIY USB adapter using a male DE-9 connector. This is simple to make and does not require any modifications to the shifter. The above video walks you through the process of wiring to an Arduino Leonardo. If you want something more robust, an open source shield is available to connect the shifter to a [SparkFun Pro Micro](https://github.com/sparkfun/Pro_Micro). The design comes with a 3D printable case and custom board files so that the device appears as a "Sim Racing Shifter" over USB. You can use this shield to build an inexpensive USB HID adapter. diff --git a/docs/pages/devices/logitech_shifter_g25.md b/docs/pages/devices/logitech_shifter_g25.md new file mode 100644 index 0000000..53dce68 --- /dev/null +++ b/docs/pages/devices/logitech_shifter_g25.md @@ -0,0 +1,100 @@ +# Logitech G25 Shifter {#logitech_shifter_g25} + +The [Logitech G25](https://en.wikipedia.org/wiki/Logitech_G25) shifter is implemented using the SimRacing::LogitechShifterG25 class. See the LogitechShifterG25_Print.ino and LogitechShifterG25_Joystick.ino examples for reference. + +The G25 shifter is near-identical to the [G27 shifter](@ref logitech_shifter_g27). It includes a "sequential" shifting mode, and pins 1 and 7 of the connector are swapped (respectively: power/clock for the G25, clock/power for the G27). These pin swaps are done in the wiring between the DE-9 and internal J11 connector; the circuit board appears to be identical. + +These notes are based off of disassembling my own G25 shifter, with the internal PCB marked "202339-0000 REV. A1". + +## Adapters + +@youtube_embed{https://www.youtube.com/embed/BVbpuYmPmm0} + +You can build your own DIY USB adapter using a male DE-9 connector. This is simple to make and does not require any modifications to the shifter. The above video walks you through the process of wiring to an Arduino Leonardo. + +If you want something more robust, an open source shield is available to connect the shifter to a [SparkFun Pro Micro](https://github.com/sparkfun/Pro_Micro). The design comes with a 3D printable case and custom board files so that the device appears as a "Sim Racing Shifter" over USB. You can use this shield to build an inexpensive USB HID adapter. + +You can find all of the necessary files in [the project repository](https://github.com/dmadison/Sim-Racing-Shields). + +## Connector + +| ![DE-9_Male](DE9_Male.svg) | ![DE-9_Female](DE9_Female.svg) | +| :-----------------------: | :---------------------------: | +| DE-9 Male Connector | DE-9 Female connector | + +DE-9 graphic from [Aeroid](https://commons.wikimedia.org/wiki/User:Aeroid) @ [Wikimedia Commons](https://commons.wikimedia.org/wiki/File:DE9_Diagram.svg#/media/File:DE-9_Female.svg), modified for scale, colors, and creation of a complementary male version. These graphics are licensed under [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/). + +The Logitech G25 shifter connects to the wheel base unit using [a female DE-9 connector](https://en.wikipedia.org/wiki/D-subminiature). Note that most jumper wires with [DuPont headers](https://en.wikipedia.org/wiki/Jump_wire) will not fit snugly into a DE-9 connector. For reliability and ease of use it's recommended to use a mating male DE-9 connector when interfacing with the shifter. + +Note that the DE-9 connector is often erroneously referred to as DB-9. These are the same thing. + +## Pinout + +| Function | DE-9 Pin | Internal J11 Pin | Data Direction | Wire Color | Necessary | Recommended Pin | +|---------------------------|----------|------------------|----------------|------------------|-----------|-----------------| +| Power | 1 | 1 | - | Black | | | +| Data Output (SDO) | 2 | 7 | Out | Gray | X | 7 | +| Latch / Chip Select | 3 | 5 | In | Yellow | X | 5 | +| X Axis Wiper | 4 | 3 | Out | Orange | X | A0 | +| Data In (SDI) / Power LED | 5 | 2 | In | Red | | | +| Ground | 6 | 8 | - | Black (Sheathed) | X | GND | +| Clock (SCLK) | 7 | 6 | In | Purple | X | 6 | +| Y Axis Wiper | 8 | 4 | Out | Green | X | A2 | +| Power | 9 | 1 | - | Black | X | VCC | + +Pin #2 (Data Output) is connected directly to the output of the EEPROM, and connected to the output of the shift registers through a 1000 Ohm resistor. + +Pin #3 (Latch / Chip Select) is shared between the onboard EEPROM and the shift registers. It is floating but should typically be held HIGH by the microcontroller. It must be pulsed once (HIGH / LOW / HIGH) to latch the data into the shift registers. Holding it LOW instructs the EEPROM to listen for commands. + +Pin #5 (Data In) is used exclusively by the EEPROM. It is also connected to the "Power" LED through a 330 Ohm resistor. Driving this pin LOW will turn on the LED. As the "Sequential Mode" LED is connected using a 470 Ohm resistor, I would recommend using a 100-120 Ohm resistor in series so that the pair are closer in brightness. + +Pin #7 (Clock) is connected to the clock inputs of both the EEPROM and the shift registers. It is floating but should typically be held LOW by the microcontroller. Pulsing the pin HIGH (LOW / HIGH / LOW) will shift one bit of data. Be wary of driving this pin to ground without protection, as this pin is also used as a joint power input for the [Logitech Driving Force Shifter](@ref logitech_shifter) and the [Logitech G27 Shifter](@ref logitech_shifter_g27). + +The power pins (#1 / #9) are connected together within the DE-9 connector. Either one can be used, but it is recommended to use pin 9 for compatibility with the other shifters. + +The shifter's electronics are theoretically compatibile with both 3.3V and 5V logic. Be sure to use the appropriate voltage for the logic level of your microcontroller. + +## Buttons + +The shifter includes 12 user-facing buttons: + +* Four black buttons in a diamond pattern +* One directional pad (D-Pad) +* Four red buttons in a straight line + +The shifter also contains two internal buttons: a button on the bottom of the shift column to indicate that it's in reverse, and a button on the sequential mode dial to indicate that it's in sequential mode. + +### Shift Registers + +These buttons are connected to the external DE-9 connector through a pair of NXP 74HC165D parallel-to-serial shift registers. + +| Button | Register | Bit | Offset | Enum | +|-----------------------|----------|-----|--------|--------------------------------------------------| +| (Unused) | Bottom | D7 | 15 | SimRacing::LogitechShifterG27::BUTTON_UNUSED1 | +| Reverse | Bottom | D6 | 14 | SimRacing::LogitechShifterG27::BUTTON_REVERSE | +| (Unused) | Bottom | D5 | 13 | SimRacing::LogitechShifterG27::BUTTON_UNUSED2 | +| Sequential Mode | Bottom | D4 | 12 | SimRacing::LogitechShifterG27::BUTTON_SEQUENTIAL | +| Red #3 | Bottom | D3 | 11 | SimRacing::LogitechShifterG27::BUTTON_3 | +| Red #2 | Bottom | D2 | 10 | SimRacing::LogitechShifterG27::BUTTON_2 | +| Red #4 | Bottom | D1 | 9 | SimRacing::LogitechShifterG27::BUTTON_4 | +| Red #1 | Bottom | D0 | 8 | SimRacing::LogitechShifterG27::BUTTON_1 | +| Black Up | Top | D7 | 7 | SimRacing::LogitechShifterG27::BUTTON_NORTH | +| Black Right | Top | D6 | 6 | SimRacing::LogitechShifterG27::BUTTON_EAST | +| Black Left | Top | D5 | 5 | SimRacing::LogitechShifterG27::BUTTON_WEST | +| Black Down | Top | D4 | 4 | SimRacing::LogitechShifterG27::BUTTON_SOUTH | +| Directional Pad Right | Top | D3 | 3 | SimRacing::LogitechShifterG27::DPAD_RIGHT | +| Directional Pad Left | Top | D2 | 2 | SimRacing::LogitechShifterG27::DPAD_LEFT | +| Directional Pad Down | Top | D1 | 1 | SimRacing::LogitechShifterG27::DPAD_DOWN | +| Directional Pad Up | Top | D0 | 0 | SimRacing::LogitechShifterG27::DPAD_UP | + +Data from the shift registers can be read using the Data Output (DE-9 #2), Latch (DE-9 #3), and Clock (DE-9 #7) pins. The latch must be pulsed LOW (HIGH / LOW / HIGH), then data read out via the data output pin while the clock is pulsed repeatedly from LOW to HIGH. + +All buttons will report a '1' state if they are pressed, and a '0' state if they are unpressed. Internally, all of these buttons are held to ground with 10k pull-downs. + +The red buttons are numbered from left to right, 1-4. The black buttons use cardinal directions. + +## EEPROM Storage + +The Logitech shifter has an internal EEPROM chip, presumably for storing settings and calibration data. In my shifter this is an [ST Microelectronics M95010-W](https://www.st.com/resource/en/datasheet/m95010-w.pdf) in an SO8 package. It has 1 Kbit of memory and can be read and written to via the DE-9 connector. The EERPOM does not need to be used in order to retrieve the control surface data from the shifter. + +This library does not implement EEPROM support, either for reading from the EEPROM or utilizing its data. diff --git a/docs/pages/devices/logitech_shifter_g27.md b/docs/pages/devices/logitech_shifter_g27.md new file mode 100644 index 0000000..c63a81b --- /dev/null +++ b/docs/pages/devices/logitech_shifter_g27.md @@ -0,0 +1,100 @@ +# Logitech G27 Shifter {#logitech_shifter_g27} + +The [Logitech G27](https://en.wikipedia.org/wiki/Logitech_G27) shifter is implemented using the SimRacing::LogitechShifterG27 class. See the LogitechShifterG27_Print.ino and LogitechShifterG27_Joystick.ino examples for reference. + +The G27 shifter is near-identical to the [G25 shifter](@ref logitech_shifter_g25). It is missing the "sequential" mode switch and mechanics, and pins 1 and 7 of the connector are swapped (respectively: power/clock for the G25, clock/power for the G27). These pin swaps are done in the wiring between the DE-9 and internal J11 connector; the circuit board appears to be identical (including unpopulated pads for the sequential mode switch and sequential mode LED). + +These notes are based off of disassembling my own G27 shifter, with the internal PCB marked "210-001096 REV. 001". + +## Adapters + +@youtube_embed{https://www.youtube.com/embed/1yXbaHrBhXQ} + +You can build your own DIY USB adapter using a male DE-9 connector. This is simple to make and does not require any modifications to the shifter. The above video walks you through the process of wiring to an Arduino Leonardo. + +If you want something more robust, an open source shield is available to connect the shifter to a [SparkFun Pro Micro](https://github.com/sparkfun/Pro_Micro). The design comes with a 3D printable case and custom board files so that the device appears as a "Sim Racing Shifter" over USB. You can use this shield to build an inexpensive USB HID adapter. + +You can find all of the necessary files in [the project repository](https://github.com/dmadison/Sim-Racing-Shields). + +## Connector + +| ![DE-9_Male](DE9_Male.svg) | ![DE-9_Female](DE9_Female.svg) | +| :-----------------------: | :---------------------------: | +| DE-9 Male Connector | DE-9 Female connector | + +DE-9 graphic from [Aeroid](https://commons.wikimedia.org/wiki/User:Aeroid) @ [Wikimedia Commons](https://commons.wikimedia.org/wiki/File:DE9_Diagram.svg#/media/File:DE-9_Female.svg), modified for scale, colors, and creation of a complementary male version. These graphics are licensed under [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/). + +The Logitech G27 shifter connects to the wheel base unit using [a female DE-9 connector](https://en.wikipedia.org/wiki/D-subminiature). Note that most jumper wires with [DuPont headers](https://en.wikipedia.org/wiki/Jump_wire) will not fit snugly into a DE-9 connector. For reliability and ease of use it's recommended to use a mating male DE-9 connector when interfacing with the shifter. + +Note that the DE-9 connector is often erroneously referred to as DB-9. These are the same thing. + +## Pinout + +| Function | DE-9 Pin | Internal J11 Pin | Data Direction | Wire Color | Necessary | Recommended Pin | +|---------------------------|----------|------------------|----------------|------------------|-----------|-----------------| +| Clock (SCLK) | 1 | 6 | In | Purple | X | 6 | +| Data Output (SDO) | 2 | 7 | Out | Gray | X | 7 | +| Latch / Chip Select | 3 | 5 | In | Yellow | X | 5 | +| X Axis Wiper | 4 | 3 | Out | Orange | X | A0 | +| Data In (SDI) / Power LED | 5 | 2 | In | White | | | +| Ground | 6 | 8 | - | Black (Sheathed) | X | GND | +| Power | 7 | 1 | - | Red | | | +| Y Axis Wiper | 8 | 4 | Out | Green | X | A2 | +| Power | 9 | 1 | - | Red | X | VCC | + +Pin #1 (Clock) is connected to the clock inputs of both the EEPROM and the shift registers. It is floating but should typically be held LOW by the microcontroller. Pulsing the pin HIGH (LOW / HIGH / LOW) will shift one bit of data. Be wary of driving this pin to ground without protection, as this pin is also used as a joint power input for the [Logitech G25 Shifter](@ref logitech_shifter_g25). + +Pin #2 (Data Output) is connected directly to the output of the EEPROM, and connected to the output of the shift registers through a 1000 Ohm resistor. + +Pin #3 (Latch / Chip Select) is shared between the onboard EEPROM and the shift registers. It is floating but should typically be held HIGH by the microcontroller. It must be pulsed once (HIGH / LOW / HIGH) to latch the data into the shift registers. Holding it LOW instructs the EEPROM to listen for commands. + +Pin #5 (Data In) is used exclusively by the EEPROM. It is also connected to the "Power" LED through a 330 Ohm resistor. Driving this pin LOW will turn on the LED. + +The power pins (#7 / #9) are connected together within the DE-9 connector. Either one can be used, but it is recommended to use pin 9 for compatibility with the other shifters. + +The shifter's electronics are theoretically compatibile with both 3.3V and 5V logic. Be sure to use the appropriate voltage for the logic level of your microcontroller. + +## Buttons + +The shifter includes 12 user-facing buttons: + +* Four black buttons in a diamond pattern +* One directional pad (D-Pad) +* Four red buttons in a straight line + +The shifter also contains one internal button on the bottom of the shift column to indicate that it's in reverse. + +### Shift Registers + +These buttons are connected to the external DE-9 connector through a pair of NXP 74HC165D parallel-to-serial shift registers. + +| Button | Register | Bit | Offset | Enum | +|-----------------------|----------|-----|--------|--------------------------------------------------| +| (Unused) | Bottom | D7 | 15 | SimRacing::LogitechShifterG27::BUTTON_UNUSED1 | +| Reverse | Bottom | D6 | 14 | SimRacing::LogitechShifterG27::BUTTON_REVERSE | +| (Unused) | Bottom | D5 | 13 | SimRacing::LogitechShifterG27::BUTTON_UNUSED2 | +| Sequential Mode | Bottom | D4 | 12 | SimRacing::LogitechShifterG27::BUTTON_SEQUENTIAL | +| Red #3 | Bottom | D3 | 11 | SimRacing::LogitechShifterG27::BUTTON_3 | +| Red #2 | Bottom | D2 | 10 | SimRacing::LogitechShifterG27::BUTTON_2 | +| Red #4 | Bottom | D1 | 9 | SimRacing::LogitechShifterG27::BUTTON_4 | +| Red #1 | Bottom | D0 | 8 | SimRacing::LogitechShifterG27::BUTTON_1 | +| Black Up | Top | D7 | 7 | SimRacing::LogitechShifterG27::BUTTON_NORTH | +| Black Right | Top | D6 | 6 | SimRacing::LogitechShifterG27::BUTTON_EAST | +| Black Left | Top | D5 | 5 | SimRacing::LogitechShifterG27::BUTTON_WEST | +| Black Down | Top | D4 | 4 | SimRacing::LogitechShifterG27::BUTTON_SOUTH | +| Directional Pad Right | Top | D3 | 3 | SimRacing::LogitechShifterG27::DPAD_RIGHT | +| Directional Pad Left | Top | D2 | 2 | SimRacing::LogitechShifterG27::DPAD_LEFT | +| Directional Pad Down | Top | D1 | 1 | SimRacing::LogitechShifterG27::DPAD_DOWN | +| Directional Pad Up | Top | D0 | 0 | SimRacing::LogitechShifterG27::DPAD_UP | + +Data from the shift registers can be read using the Data Output (DE-9 #2), Latch (DE-9 #3), and Clock (DE-9 #1) pins. The latch must be pulsed LOW (HIGH / LOW / HIGH), then data read out via the data output pin while the clock is pulsed repeatedly from LOW to HIGH. + +All buttons will report a '1' state if they are pressed, and a '0' state if they are unpressed. Internally, all of these buttons are held to ground with 10k pull-downs. + +The red buttons are numbered from left to right, 1-4. The black buttons use cardinal directions. + +## EEPROM Storage + +The Logitech shifter has an internal EEPROM chip, presumably for storing settings and calibration data. In my shifter this is an [ST Microelectronics M95010-W](https://www.st.com/resource/en/datasheet/m95010-w.pdf) in an SO8 package. It has 1 Kbit of memory and can be read and written to via the DE-9 connector. The EERPOM does not need to be used in order to retrieve the control surface data from the shifter. + +This library does not implement EEPROM support, either for reading from the EEPROM or utilizing its data. diff --git a/docs/pages/supported_devices.md b/docs/pages/supported_devices.md index 0761f5a..f70b18f 100644 --- a/docs/pages/supported_devices.md +++ b/docs/pages/supported_devices.md @@ -11,3 +11,5 @@ - @subpage logitech_pedals - @subpage logitech_shifter +- @subpage logitech_shifter_g27 +- @subpage logitech_shifter_g25 diff --git a/docs/pages/usb_adapter_faq.md b/docs/pages/usb_adapter_faq.md new file mode 100644 index 0000000..8e9c490 --- /dev/null +++ b/docs/pages/usb_adapter_faq.md @@ -0,0 +1,102 @@ +# USB Adapter FAQ {#usb_adapter_faq} + +This page constains answers to frequently asked questions (FAQ) about building USB adapters using the [Sim Racing Library for Arduino](https://github.com/dmadison/Sim-Racing-Arduino). + +You can find my tutorial videos for building your own USB adapters [on YouTube](https://www.youtube.com/playlist?list=PLTboGmshZ5EIWQSYEjdrIFqgc2J6sLCSa). + + +### I have a G923 / G920 / G29 shifter. Why do you call it the "Driving Force" shifter? + +[That's what Logitech calls it](https://www.logitechg.com/en-us/products/driving/driving-force-shifter.html). + + +### What shifters are compatible with these DIY USB adapters? + +I have made DIY USB adapter tutorial videos for the [Logitech Driving Force shifter](@ref logitech_shifter), the [Logitech G27 shifter](@ref logitech_shifter_g27), and the [Logitech G25 shifter](@ref logitech_shifter_g25). + +All of these adapters are **only** compatible with the advertised shifter (i.e. if you made an adapter for the G25, it's only compatible with the G25). The exception to this is the G27 adapter, which is forwards-compatible with the Driving Force shifter. Do not connect an incompatible shifter to your adapter, you may damage the shifter or the Arduino! + + +### Can I make an adapter that supports all three shifters? + +Yes, but it requires some additional resistors. The [Sim Racing Shifter Shield](https://github.com/dmadison/Sim-Racing-Shields/) I designed supports all three shifters, as does its included firmware. Take a look at the schematic for reference. + + +### Why is the power LED off for my G25 / G27 adapter? How do I turn it on? + +The power LED is controlled separately, and isn't required for the adapter to function (which is why it's not included in the tutorial video). + +The control pin for the power LED is DE-9 pin 5. If you connect that pin to ground (GND) on the Arduino, the LED will always be on. If you connect that pin to one of the Arduino's I/O pins and pass the pin number to the shifter object's constructor, you can control its state using the `SimRacing::LogitechShifterG27::setPowerLED(bool)` function. + +On the G25, I would recommend adding a 100 Ohm resistor in series so that the power and sequential mode LEDs are similar in brightness. You can add a series resistor on either shifter to reduce the brightness of the LED. + + +### Can I make a USB adapter with both a shifter and pedals? + +Yes! Although you'll need to get your hands a little dirty with the code. + +You'll need to change the wiring to the Arduino board so that you can fit both peripherals. None of the devices are particularly picky about which pins you use, so there's a lot of flexibility there. The only restrictions are that the power pins (5V/GND) need to stay the same, and the analog outputs need to connect to the analog pins (A0-A5). I would recommend changing the pins on the existing examples and testing them out before continuing further. + +You will also need to combine the two example codes together. Believe me, this is easier than it sounds. I'd recommend opening two code windows side-by-side and copying between them. Add the new lines, remove duplicate lines, and if two lines are different do your best to reason out why that is. The most complicated bit is the `Joystick` object definition, which needs to change based on what you're outputting to USB. Hopefully the comments (the `//` bits) are clear enough. + +The library itself does not need to be modified, only the example code. If you run into any trouble there are a number of great learning resources online, including [the Arduino forums](https://forum.arduino.cc/) and [the Arduino subreddit](https://www.reddit.com/r/arduino). Good luck! + + +### Can I use an Arduino Uno / Arduino Nano / Arduino Mega instead of an Arduino Leonardo? + +Short answer: **No**. + +Long answer: *Maybe*. But it requires a specific variant of the board and it's significantly more effort. + +The tutorials suggest using an [Arduino Leonardo](https://docs.arduino.cc/hardware/leonardo/) because the onboard microcontroller (the ATmega32U4) has a hardware USB controller and female headers that don't require soldering. This means that the library code can set the USB descriptors and tell the microcontroller to behave as a USB human interface device (HID), then control the output based on the sim racing peripheral. + +In contrast, the microcontrollers on the [Arduino Uno (ATmega328P)](https://docs.arduino.cc/hardware/uno-rev3/), [Arduino Nano (ATmega328P)](https://docs.arduino.cc/hardware/nano/), and [Arduino Mega (ATmega2560)](https://docs.arduino.cc/hardware/mega-2560/) do **not** have a USB controller. Instead, they use a secondary integrated circuit (IC) to convert the serial data (UART) into USB. This means that the library code *cannot* set the USB descriptors to tell the microcontroller to behave as a USB HID device. + +On the Arduino Nano and on most low cost clones of the Arduino Uno and Arduino Mega, that IC is the [FT232](https://ftdichip.com/products/ft232rl/) or a knockoff like the CH340. These chips are unable to act as a USB adapter. + +On genuine and more expensive Arduino Unos and Arduino Megas, that IC is the ATmega16U2 - another microcontroller, and the brother of the ATmega32U4 onboard the Leonardo. With these boards it *is* technically possible to use them as a USB adapter, although it's not easy. + +To convert the ATmega16U2 into an HID device you can install the [UnoJoy](https://github.com/AlanChatham/UnoJoy) firmware. This is tricky to do over USB (using "Device Firmware Update" / DFU mode), but can be simplified using a hardware ICSP programmer. There are further instructions in that repository's documentation. + +You will also need to modify the library examples to use the UnoJoy library in place of the Joystick library to send serial commands to the ATmega16U2. This should be relatively straightforward, but you will need to understand some basic C/C++ programming. + +To recap, the process is: + + 1. Inspect the board to verify that you have a compatible model (Arduino Uno or Arduino Mega with an ATmega16U2 as the USB to serial interface) + 2. Reset the ATmega16U2 into DFU mode by shorting the reset pins on the ICSP header (or) attach an external hardware programmer + 3. Flash UnoJoy firmware to the ATmega16U2 + 4. Rewrite the library examples to use the UnoJoy library in place of the Joystick library for USB output + 5. Upload the code to the Uno (ATmega328P) + +Fair warning that you may have some difficulty getting the ATmega16U2 into DFU mode, and you may have some difficulty getting it back to functioning as a "regular" Arduino board afterwards. It is also possible to "brick" the board and render it in an unusable state. This is often recoverable but requires a hardware ICSP programmer. + + +### Can I use ______ microcontroller instead? + +Maybe! The library itself should be compatible with most development boards that support the Arduino framework. Try to compile one of the library examples for your microcontroller. If it compiles, it will probably work! + +The [Joystick library](https://github.com/MHeironimus/ArduinoJoystickLibrary/) supports a more limited subset of boards, and you may need to rewrite the Sim Racing Library example code to use a different USB library. + + +### The Arduino Leonardo is really big. Is there a smaller board I can use? + +Yes! The [SparkFun Pro Micro (ATmega32U4)](https://www.sparkfun.com/sparkfun-qwiic-pro-micro-usb-c-atmega32u4.html) has the same microcontroller and the same pinout. It's also the microcontroller that's compatible with the [Sim Racing Shields](https://github.com/dmadison/Sim-Racing-Shields/). But because it doesn't have female headers, you must solder to the pins. + + +### The SparkFun Pro Micro doesn't have a 5V pin. Which pin should I use? + +You should use the VCC pin for power in place of the 5V pin. + + +### I don't know how to solder. How do I connect multiple wires to one pin? + +I would recommend using [Wago 221 Series](https://www.wago.com/us/f/222-series-lever-nuts) or [Wago 222 Series Lever-Nuts](https://www.wago.com/us/f/221-series-levernuts). You can find them at many hardware stores, they're easy to use and make a solid connection. + +You can also use a more traditional solderless breadboard. These are common prototyping tools, but they aren't a great long term solution as the wires can come loose. + + +### Why can't I just use a DE-9 to USB adapter? + +The [DE-9 connector](https://en.wikipedia.org/wiki/D-subminiature) (also commonly called the "DB9" connector) has historically been used for serial devices using the [RS-232 standard](https://en.wikipedia.org/wiki/RS-232). Most common "DE-9 to USB" adapters you will find are, in fact, RS-232 to USB adapters. + +Although they use the DE-9 connector, none of the Logitech sim racing peripherals in this library are RS-232 devices. At best, the adapter will not work. At worst, you may damage the adapter or your sim racing peripheral. diff --git a/examples/Pedals/PedalsJoystick/PedalsJoystick.ino b/examples/Pedals/PedalsJoystick/PedalsJoystick.ino index a50770a..845f1f7 100644 --- a/examples/Pedals/PedalsJoystick/PedalsJoystick.ino +++ b/examples/Pedals/PedalsJoystick/PedalsJoystick.ino @@ -36,7 +36,7 @@ const int Pin_Brake = A1; const int Pin_Clutch = A0; SimRacing::LogitechPedals pedals(Pin_Gas, Pin_Brake, Pin_Clutch); -//SimRacing::LogitechPedals pedals(PEDAL_SHIELD_V1_PINS); +//SimRacing::LogitechPedals pedals = SimRacing::CreateShieldObject(); Joystick_ Joystick( JOYSTICK_DEFAULT_REPORT_ID, // default report (no additional pages) diff --git a/examples/Pedals/PedalsPrint/PedalsPrint.ino b/examples/Pedals/PedalsPrint/PedalsPrint.ino index 395d7a8..474024a 100644 --- a/examples/Pedals/PedalsPrint/PedalsPrint.ino +++ b/examples/Pedals/PedalsPrint/PedalsPrint.ino @@ -32,7 +32,7 @@ const int Pin_Brake = A1; const int Pin_Clutch = A0; SimRacing::LogitechPedals pedals(Pin_Gas, Pin_Brake, Pin_Clutch); -//SimRacing::LogitechPedals pedals(PEDAL_SHIELD_V1_PINS); +//SimRacing::LogitechPedals pedals = SimRacing::CreateShieldObject(); void setup() { diff --git a/examples/Shifter/ShiftJoystick/ShiftJoystick.ino b/examples/Shifter/LogitechShifter/LogitechShifter_Joystick/LogitechShifter_Joystick.ino similarity index 79% rename from examples/Shifter/ShiftJoystick/ShiftJoystick.ino rename to examples/Shifter/LogitechShifter/LogitechShifter_Joystick/LogitechShifter_Joystick.ino index 6b36ef6..b66472d 100644 --- a/examples/Shifter/ShiftJoystick/ShiftJoystick.ino +++ b/examples/Shifter/LogitechShifter/LogitechShifter_Joystick/LogitechShifter_Joystick.ino @@ -21,8 +21,9 @@ */ /** - * @details Emulates the shifter as a joystick over USB. - * @example ShiftJoystick.ino + * @details Emulates the Logitech Driving Force shifter (included with + * the G923 / G920 / G29 wheels) as a joystick over USB. + * @example LogitechShifter_Joystick.ino */ // This example requires the Arduino Joystick Library @@ -40,12 +41,23 @@ const bool SendAnalogAxis = false; // games, but can be useful for custom controller purposes. const bool SendReverseRaw = false; -const int Pin_ShifterX = A0; -const int Pin_ShifterY = A2; -const int Pin_ShifterRev = 2; - -SimRacing::LogitechShifter shifter(Pin_ShifterX, Pin_ShifterY, Pin_ShifterRev); -//SimRacing::LogitechShifter shifter(SHIFTER_SHIELD_V1_PINS); +// Power (VCC): DE-9 pin 9 +// Ground (GND): DE-9 pin 6 +// Note: DE-9 pin 3 (CS) needs to be pulled-up to VCC! +const int Pin_ShifterX = A0; // DE-9 pin 4 +const int Pin_ShifterY = A2; // DE-9 pin 8 +const int Pin_ShifterRev = 2; // DE-9 pin 2 + +// This pin requires an extra resistor! If you have made the proper +// connections, change the pin number to the one you're using +const int Pin_ShifterDetect = SimRacing::UnusedPin; // DE-9 pin 7, requires pull-down resistor + +SimRacing::LogitechShifter shifter( + Pin_ShifterX, Pin_ShifterY, + Pin_ShifterRev, + Pin_ShifterDetect +); +//SimRacing::LogitechShifter shifter = SimRacing::CreateShieldObject(); const int Gears[] = { 1, 2, 3, 4, 5, 6, -1 }; const int NumGears = sizeof(Gears) / sizeof(Gears[0]); diff --git a/examples/Shifter/ShiftPrint/ShiftPrint.ino b/examples/Shifter/LogitechShifter/LogitechShifter_Print/LogitechShifter_Print.ino similarity index 69% rename from examples/Shifter/ShiftPrint/ShiftPrint.ino rename to examples/Shifter/LogitechShifter/LogitechShifter_Print/LogitechShifter_Print.ino index dbabb6f..0434e1b 100644 --- a/examples/Shifter/ShiftPrint/ShiftPrint.ino +++ b/examples/Shifter/LogitechShifter/LogitechShifter_Print/LogitechShifter_Print.ino @@ -21,18 +21,30 @@ */ /** - * @details Reads and prints the current gear over serial. - * @example ShiftPrint.ino + * @details Reads from the Logitech Driving Force shifter (included with + * the G923 / G920 / G29 wheels) and prints the data over serial. + * @example LogitechShifter_Print.ino */ #include -const int Pin_ShifterX = A0; -const int Pin_ShifterY = A2; -const int Pin_ShifterRev = 2; - -SimRacing::LogitechShifter shifter(Pin_ShifterX, Pin_ShifterY, Pin_ShifterRev); -//SimRacing::LogitechShifter shifter(SHIFTER_SHIELD_V1_PINS); +// Power (VCC): DE-9 pin 9 +// Ground (GND): DE-9 pin 6 +// Note: DE-9 pin 3 (CS) needs to be pulled-up to VCC! +const int Pin_ShifterX = A0; // DE-9 pin 4 +const int Pin_ShifterY = A2; // DE-9 pin 8 +const int Pin_ShifterRev = 2; // DE-9 pin 2 + +// This pin requires an extra resistor! If you have made the proper +// connections, change the pin number to the one you're using +const int Pin_ShifterDetect = SimRacing::UnusedPin; // DE-9 pin 7, requires pull-down resistor + +SimRacing::LogitechShifter shifter( + Pin_ShifterX, Pin_ShifterY, + Pin_ShifterRev, + Pin_ShifterDetect +); +//SimRacing::LogitechShifter shifter = SimRacing::CreateShieldObject(); const unsigned long PrintSpeed = 1500; // ms unsigned long lastPrint = 0; diff --git a/examples/Shifter/LogitechShifterG25/LogitechShifterG25_Joystick/LogitechShifterG25_Joystick.ino b/examples/Shifter/LogitechShifterG25/LogitechShifterG25_Joystick/LogitechShifterG25_Joystick.ino new file mode 100644 index 0000000..3b5602e --- /dev/null +++ b/examples/Shifter/LogitechShifterG25/LogitechShifterG25_Joystick/LogitechShifterG25_Joystick.ino @@ -0,0 +1,163 @@ +/* + * Project Sim Racing Library for Arduino + * @author David Madison + * @link github.com/dmadison/Sim-Racing-Arduino + * @license LGPLv3 - Copyright (c) 2024 David Madison + * + * This file is part of the Sim Racing Library for Arduino. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + + /** + * @details Emulates the Logitech G25 shifter as a joystick over USB. + * @example LogitechShifterG25_Joystick.ino + */ + +// This example requires the Arduino Joystick Library +// Download Here: https://github.com/MHeironimus/ArduinoJoystickLibrary + +#include +#include + +// Power (VCC): DE-9 pin 9 +// Ground (GND): DE-9 pin 6 +const int Pin_ShifterX = A0; // DE-9 pin 4 +const int Pin_ShifterY = A2; // DE-9 pin 8 + +const int Pin_ShifterLatch = 5; // DE-9 pin 3 +const int Pin_ShifterClock = 6; // DE-9 pin 7 +const int Pin_ShifterData = 7; // DE-9 pin 2 + +// This pin is optional! You do not need to connect it in order +// to read data from the shifter. Connecting it and changing the +// pin number below will light the power LED. On the G25, I +// recommend using a 100 Ohm resistor in series to match the +// brightness of the sequential mode LED. +const int Pin_ShifterLED = SimRacing::UnusedPin; // DE-9 pin 5 + +// This pin requies a pull-down resistor! If you have made the proper +// connections, change the pin number to the one you're using. Setting +// it will zero data when the shifter is disconnected. +const int Pin_ShifterDetect = SimRacing::UnusedPin; // DE-9 pin 1 + +SimRacing::LogitechShifterG25 shifter( + Pin_ShifterX, Pin_ShifterY, + Pin_ShifterLatch, Pin_ShifterClock, Pin_ShifterData, + Pin_ShifterLED, Pin_ShifterDetect +); +//SimRacing::LogitechShifterG25 shifter = SimRacing::CreateShieldObject(); + +// Set this option to 'true' to send the shifter's X/Y position +// as a joystick. This is not needed for most games. +const bool SendAnalogAxis = false; + +const int Gears[] = { 1, 2, 3, 4, 5, 6, -1 }; +const int NumGears = sizeof(Gears) / sizeof(Gears[0]); + +using ShifterButton = SimRacing::LogitechShifterG25::Button; +const ShifterButton Buttons[] = { + ShifterButton::BUTTON_SOUTH, + ShifterButton::BUTTON_EAST, + ShifterButton::BUTTON_WEST, + ShifterButton::BUTTON_NORTH, + ShifterButton::BUTTON_1, + ShifterButton::BUTTON_2, + ShifterButton::BUTTON_3, + ShifterButton::BUTTON_4, +}; +const int NumButtons = sizeof(Buttons) / sizeof(Buttons[0]); + +const int ADC_Max = 1023; // 10-bit on AVR + +Joystick_ Joystick( + JOYSTICK_DEFAULT_REPORT_ID, // default report (no additional pages) + JOYSTICK_TYPE_JOYSTICK, // so that this shows up in Windows joystick manager + NumGears + NumButtons + 2, // number of buttons (7 gears: reverse and 1-6, 8 buttons, 2 sequential gears) + 1, // number of hat switches (1, the directional pad) + SendAnalogAxis, SendAnalogAxis, // include X and Y axes for analog output, if set above + false, false, false, false, false, false, false, false, false); // no other axes + +void updateJoystick(); // forward-declared function for non-Arduino environments + + +void setup() { + shifter.begin(); + + // if you have one, your calibration line should go here + + Joystick.begin(false); // 'false' to disable auto-send + Joystick.setXAxisRange(0, ADC_Max); + Joystick.setYAxisRange(ADC_Max, 0); // invert axis so 'up' is up + + updateJoystick(); // send initial state +} + +void loop() { + bool dataChanged = shifter.update(); + + if (dataChanged || SendAnalogAxis == true) { + updateJoystick(); + } +} + +void updateJoystick() { + // keep track of which button we're updating + // in the joystick output + int currentButton = 0; + + // set the buttons corresponding to the gears + for (int i = 0; i < NumGears; i++) { + if (shifter.getGear() == Gears[i]) { + Joystick.pressButton(currentButton); + } + else { + Joystick.releaseButton(currentButton); + } + + currentButton++; + } + + // set the analog axes (if the option is set) + if (SendAnalogAxis == true) { + int x = shifter.getPosition(SimRacing::X, 0, ADC_Max); + int y = shifter.getPosition(SimRacing::Y, 0, ADC_Max); + Joystick.setXAxis(x); + Joystick.setYAxis(y); + } + + // set the buttons + for (int i = 0; i < NumButtons; i++) { + bool state = shifter.getButton(Buttons[i]); + Joystick.setButton(currentButton, state); + + currentButton++; + } + + // set the hatswitch (directional pad) + int angle = shifter.getDpadAngle(); + Joystick.setHatSwitch(0, angle); + + // set the sequential shifting buttons + bool shiftUp = shifter.getShiftUp(); + Joystick.setButton(currentButton, shiftUp); + currentButton++; + + bool shiftDown = shifter.getShiftDown(); + Joystick.setButton(currentButton, shiftDown); + currentButton++; + + // send the updated data via USB + Joystick.sendState(); +} diff --git a/examples/Shifter/LogitechShifterG25/LogitechShifterG25_Print/LogitechShifterG25_Print.ino b/examples/Shifter/LogitechShifterG25/LogitechShifterG25_Print/LogitechShifterG25_Print.ino new file mode 100644 index 0000000..c11f445 --- /dev/null +++ b/examples/Shifter/LogitechShifterG25/LogitechShifterG25_Print/LogitechShifterG25_Print.ino @@ -0,0 +1,163 @@ +/* + * Project Sim Racing Library for Arduino + * @author David Madison + * @link github.com/dmadison/Sim-Racing-Arduino + * @license LGPLv3 - Copyright (c) 2024 David Madison + * + * This file is part of the Sim Racing Library for Arduino. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + + /** + * @details Reads from the Logitech G25 shifter and prints the data over serial. + * @example LogitechShifterG25_Print.ino + */ + +#include + +// Power (VCC): DE-9 pin 9 +// Ground (GND): DE-9 pin 6 +const int Pin_ShifterX = A0; // DE-9 pin 4 +const int Pin_ShifterY = A2; // DE-9 pin 8 + +const int Pin_ShifterLatch = 5; // DE-9 pin 3 +const int Pin_ShifterClock = 6; // DE-9 pin 7 +const int Pin_ShifterData = 7; // DE-9 pin 2 + +// This pin is optional! You do not need to connect it in order +// to read data from the shifter. Connecting it and changing the +// pin number below will light the power LED. On the G25, I +// recommend using a 100 Ohm resistor in series to match the +// brightness of the sequential mode LED. +const int Pin_ShifterLED = SimRacing::UnusedPin; // DE-9 pin 5 + +// This pin requies a pull-down resistor! If you have made the proper +// connections, change the pin number to the one you're using. Setting +// it will zero data when the shifter is disconnected. +const int Pin_ShifterDetect = SimRacing::UnusedPin; // DE-9 pin 1 + +SimRacing::LogitechShifterG25 shifter( + Pin_ShifterX, Pin_ShifterY, + Pin_ShifterLatch, Pin_ShifterClock, Pin_ShifterData, + Pin_ShifterLED, Pin_ShifterDetect +); +//SimRacing::LogitechShifterG25 shifter = SimRacing::CreateShieldObject(); + +// alias so we don't need to type so much +using ShifterButton = SimRacing::LogitechShifterG25::Button; + +// forward-declared functions for non-Arduino environments +void printConditional(bool state, char pressed); +void printButton(ShifterButton button, char pressed); +void printShifter(); + +const unsigned long PrintSpeed = 1500; // ms +unsigned long lastPrint = 0; + + +void setup() { + shifter.begin(); + + // if you have one, your calibration line should go here + + Serial.begin(115200); + while (!Serial); // wait for connection to open + + Serial.println("Logitech G25 Starting..."); +} + +void loop() { + // send some serial data to run conversational calibration + if (Serial.read() != -1) { + shifter.serialCalibration(); + shifter.serialCalibrationSequential(); + delay(2000); + } + + bool dataChanged = shifter.update(); + + // if data has changed, print immediately + if (dataChanged) { + Serial.print("! "); + printShifter(); + } + + // otherwise, print if we've been idle for awhile + if (millis() - lastPrint >= PrintSpeed) { + Serial.print(" "); + printShifter(); + } +} + +void printConditional(bool state, char pressed) { + if (state == true) { + Serial.print(pressed); + } + else { + Serial.print('_'); + } +} + +void printButton(ShifterButton button, char pressed) { + bool state = shifter.getButton(button); + printConditional(state, pressed); +} + +void printShifter() { + // if in sequential mode, print up/down + if (shifter.inSequentialMode()) { + Serial.print("S:["); + printConditional(shifter.getShiftUp(), '+'); + printConditional(shifter.getShiftDown(), '-'); + Serial.print(']'); + } + // otherwise in H-pattern mode, print the gear + else { + Serial.print("H: ["); + Serial.print(shifter.getGearChar()); + Serial.print("]"); + } + + // print X/Y position of shifter + Serial.print(" - XY: ("); + Serial.print(shifter.getPositionRaw(SimRacing::X)); + Serial.print(", "); + Serial.print(shifter.getPositionRaw(SimRacing::Y)); + Serial.print(") "); + + // print directional pad + printButton(ShifterButton::DPAD_LEFT, '<'); + printButton(ShifterButton::DPAD_UP, '^'); + printButton(ShifterButton::DPAD_DOWN, 'v'); + printButton(ShifterButton::DPAD_RIGHT, '>'); + Serial.print(' '); + + // print black buttons + printButton(ShifterButton::BUTTON_NORTH, 'N'); + printButton(ShifterButton::BUTTON_SOUTH, 'S'); + printButton(ShifterButton::BUTTON_EAST, 'E'); + printButton(ShifterButton::BUTTON_WEST, 'W'); + Serial.print(' '); + + // print red buttons + printButton(ShifterButton::BUTTON_1, '1'); + printButton(ShifterButton::BUTTON_2, '2'); + printButton(ShifterButton::BUTTON_3, '3'); + printButton(ShifterButton::BUTTON_4, '4'); + + Serial.println(); + + lastPrint = millis(); +} diff --git a/examples/Shifter/LogitechShifterG27/LogitechShifterG27_Joystick/LogitechShifterG27_Joystick.ino b/examples/Shifter/LogitechShifterG27/LogitechShifterG27_Joystick/LogitechShifterG27_Joystick.ino new file mode 100644 index 0000000..2158e03 --- /dev/null +++ b/examples/Shifter/LogitechShifterG27/LogitechShifterG27_Joystick/LogitechShifterG27_Joystick.ino @@ -0,0 +1,152 @@ +/* + * Project Sim Racing Library for Arduino + * @author David Madison + * @link github.com/dmadison/Sim-Racing-Arduino + * @license LGPLv3 - Copyright (c) 2024 David Madison + * + * This file is part of the Sim Racing Library for Arduino. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + + /** + * @details Emulates the Logitech G27 shifter as a joystick over USB. + * @example LogitechShifterG27_Joystick.ino + */ + +// This example requires the Arduino Joystick Library +// Download Here: https://github.com/MHeironimus/ArduinoJoystickLibrary + +#include +#include + +// Power (VCC): DE-9 pin 9 +// Ground (GND): DE-9 pin 6 +const int Pin_ShifterX = A0; // DE-9 pin 4 +const int Pin_ShifterY = A2; // DE-9 pin 8 + +const int Pin_ShifterLatch = 5; // DE-9 pin 3 +const int Pin_ShifterClock = 6; // DE-9 pin 1 +const int Pin_ShifterData = 7; // DE-9 pin 2 + +// This pin is optional! You do not need to connect it in order +// to read data from the shifter. Connecting it and changing the +// pin number below will light the power LED. +const int Pin_ShifterLED = SimRacing::UnusedPin; // DE-9 pin 5 + +// This pin requies a pull-down resistor! If you have made the proper +// connections, change the pin number to the one you're using. Setting +// it will zero data when the shifter is disconnected. +const int Pin_ShifterDetect = SimRacing::UnusedPin; // DE-9 pin 7 + +SimRacing::LogitechShifterG27 shifter( + Pin_ShifterX, Pin_ShifterY, + Pin_ShifterLatch, Pin_ShifterClock, Pin_ShifterData, + Pin_ShifterLED, Pin_ShifterDetect +); +//SimRacing::LogitechShifterG27 shifter = SimRacing::CreateShieldObject(); + +// Set this option to 'true' to send the shifter's X/Y position +// as a joystick. This is not needed for most games. +const bool SendAnalogAxis = false; + +const int Gears[] = { 1, 2, 3, 4, 5, 6, -1 }; +const int NumGears = sizeof(Gears) / sizeof(Gears[0]); + +using ShifterButton = SimRacing::LogitechShifterG27::Button; +const ShifterButton Buttons[] = { + ShifterButton::BUTTON_SOUTH, + ShifterButton::BUTTON_EAST, + ShifterButton::BUTTON_WEST, + ShifterButton::BUTTON_NORTH, + ShifterButton::BUTTON_1, + ShifterButton::BUTTON_2, + ShifterButton::BUTTON_3, + ShifterButton::BUTTON_4, +}; +const int NumButtons = sizeof(Buttons) / sizeof(Buttons[0]); + +const int ADC_Max = 1023; // 10-bit on AVR + +Joystick_ Joystick( + JOYSTICK_DEFAULT_REPORT_ID, // default report (no additional pages) + JOYSTICK_TYPE_JOYSTICK, // so that this shows up in Windows joystick manager + NumGears + NumButtons, // number of buttons (7 gears: reverse and 1-6, 8 buttons) + 1, // number of hat switches (1, the directional pad) + SendAnalogAxis, SendAnalogAxis, // include X and Y axes for analog output, if set above + false, false, false, false, false, false, false, false, false); // no other axes + +void updateJoystick(); // forward-declared function for non-Arduino environments + + +void setup() { + shifter.begin(); + + // if you have one, your calibration line should go here + + Joystick.begin(false); // 'false' to disable auto-send + Joystick.setXAxisRange(0, ADC_Max); + Joystick.setYAxisRange(ADC_Max, 0); // invert axis so 'up' is up + + updateJoystick(); // send initial state +} + +void loop() { + bool dataChanged = shifter.update(); + + if (dataChanged || SendAnalogAxis == true) { + updateJoystick(); + } +} + +void updateJoystick() { + // keep track of which button we're updating + // in the joystick output + int currentButton = 0; + + // set the buttons corresponding to the gears + for (int i = 0; i < NumGears; i++) { + if (shifter.getGear() == Gears[i]) { + Joystick.pressButton(currentButton); + } + else { + Joystick.releaseButton(currentButton); + } + + currentButton++; + } + + // set the analog axes (if the option is set) + if (SendAnalogAxis == true) { + int x = shifter.getPosition(SimRacing::X, 0, ADC_Max); + int y = shifter.getPosition(SimRacing::Y, 0, ADC_Max); + Joystick.setXAxis(x); + Joystick.setYAxis(y); + } + + // set the buttons + for (int i = 0; i < NumButtons; i++) { + bool state = shifter.getButton(Buttons[i]); + Joystick.setButton(currentButton, state); + + currentButton++; + } + + // set the hatswitch (directional pad) + int angle = shifter.getDpadAngle(); + Joystick.setHatSwitch(0, angle); + + // send the updated data via USB + Joystick.sendState(); +} diff --git a/examples/Shifter/LogitechShifterG27/LogitechShifterG27_Print/LogitechShifterG27_Print.ino b/examples/Shifter/LogitechShifterG27/LogitechShifterG27_Print/LogitechShifterG27_Print.ino new file mode 100644 index 0000000..ffd567c --- /dev/null +++ b/examples/Shifter/LogitechShifterG27/LogitechShifterG27_Print/LogitechShifterG27_Print.ino @@ -0,0 +1,151 @@ +/* + * Project Sim Racing Library for Arduino + * @author David Madison + * @link github.com/dmadison/Sim-Racing-Arduino + * @license LGPLv3 - Copyright (c) 2024 David Madison + * + * This file is part of the Sim Racing Library for Arduino. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + + /** + * @details Reads from the Logitech G27 shifter and prints the data over serial. + * @example LogitechShifterG27_Print.ino + */ + +#include + +// Power (VCC): DE-9 pin 9 +// Ground (GND): DE-9 pin 6 +const int Pin_ShifterX = A0; // DE-9 pin 4 +const int Pin_ShifterY = A2; // DE-9 pin 8 + +const int Pin_ShifterLatch = 5; // DE-9 pin 3 +const int Pin_ShifterClock = 6; // DE-9 pin 1 +const int Pin_ShifterData = 7; // DE-9 pin 2 + +// This pin is optional! You do not need to connect it in order +// to read data from the shifter. Connecting it and changing the +// pin number below will light the power LED. +const int Pin_ShifterLED = SimRacing::UnusedPin; // DE-9 pin 5 + +// This pin requies a pull-down resistor! If you have made the proper +// connections, change the pin number to the one you're using. Setting +// it will zero data when the shifter is disconnected. +const int Pin_ShifterDetect = SimRacing::UnusedPin; // DE-9 pin 7 + +SimRacing::LogitechShifterG27 shifter( + Pin_ShifterX, Pin_ShifterY, + Pin_ShifterLatch, Pin_ShifterClock, Pin_ShifterData, + Pin_ShifterLED, Pin_ShifterDetect +); +//SimRacing::LogitechShifterG27 shifter = SimRacing::CreateShieldObject(); + +// alias so we don't need to type so much +using ShifterButton = SimRacing::LogitechShifterG27::Button; + +// forward-declared functions for non-Arduino environments +void printConditional(bool state, char pressed); +void printButton(ShifterButton button, char pressed); +void printShifter(); + +const unsigned long PrintSpeed = 1500; // ms +unsigned long lastPrint = 0; + + +void setup() { + shifter.begin(); + + // if you have one, your calibration line should go here + + Serial.begin(115200); + while (!Serial); // wait for connection to open + + Serial.println("Logitech G27 Starting..."); +} + +void loop() { + // send some serial data to run conversational calibration + if (Serial.read() != -1) { + shifter.serialCalibration(); + delay(2000); + } + + bool dataChanged = shifter.update(); + + // if data has changed, print immediately + if (dataChanged) { + Serial.print("! "); + printShifter(); + } + + // otherwise, print if we've been idle for awhile + if (millis() - lastPrint >= PrintSpeed) { + Serial.print(" "); + printShifter(); + } +} + +void printConditional(bool state, char pressed) { + if (state == true) { + Serial.print(pressed); + } + else { + Serial.print('_'); + } +} + +void printButton(ShifterButton button, char pressed) { + bool state = shifter.getButton(button); + printConditional(state, pressed); +} + +void printShifter() { + // print the H-pattern gear + Serial.print("H:["); + Serial.print(shifter.getGearChar()); + Serial.print("]"); + + // print X/Y position of shifter + Serial.print(" - XY: ("); + Serial.print(shifter.getPositionRaw(SimRacing::X)); + Serial.print(", "); + Serial.print(shifter.getPositionRaw(SimRacing::Y)); + Serial.print(") "); + + // print directional pad + printButton(ShifterButton::DPAD_LEFT, '<'); + printButton(ShifterButton::DPAD_UP, '^'); + printButton(ShifterButton::DPAD_DOWN, 'v'); + printButton(ShifterButton::DPAD_RIGHT, '>'); + Serial.print(' '); + + // print black buttons + printButton(ShifterButton::BUTTON_NORTH, 'N'); + printButton(ShifterButton::BUTTON_SOUTH, 'S'); + printButton(ShifterButton::BUTTON_EAST, 'E'); + printButton(ShifterButton::BUTTON_WEST, 'W'); + Serial.print(' '); + + // print red buttons + printButton(ShifterButton::BUTTON_1, '1'); + printButton(ShifterButton::BUTTON_2, '2'); + printButton(ShifterButton::BUTTON_3, '3'); + printButton(ShifterButton::BUTTON_4, '4'); + + Serial.println(); + + lastPrint = millis(); +} diff --git a/keywords.txt b/keywords.txt index 9e9c1ac..ca843ef 100644 --- a/keywords.txt +++ b/keywords.txt @@ -35,9 +35,21 @@ AnalogShifter KEYWORD1 LogitechShifter KEYWORD1 +LogitechShifterG923 KEYWORD1 +LogitechShifterG920 KEYWORD1 +LogitechShifterG29 KEYWORD1 +LogitechShifterG27 KEYWORD1 +LogitechShifterG25 KEYWORD1 + # Handbrake Classes Handbrake KEYWORD1 +####################################### +# Functions (KEYWORD2) +####################################### + +CreateShieldObject KEYWORD2 + ####################################### # Peripheral Class Methods and Functions (KEYWORD2) ####################################### @@ -46,6 +58,7 @@ begin KEYWORD2 update KEYWORD2 isConnected KEYWORD2 +setStablePeriod KEYWORD2 ####################################### # AnalogInput Class Methods and Functions (KEYWORD2) @@ -99,6 +112,57 @@ getReverseButton KEYWORD2 setCalibration KEYWORD2 serialCalibration KEYWORD2 +####################################### +# LogitechShifterG27 Datatypes (KEYWORD1) +####################################### + +# Button Enum +Button KEYWORD1 + +####################################### +# LogitechShifterG27 Constants (LITERAL1) +####################################### + +# Button Enum Values +BUTTON_UNUSED1 LITERAL1 +BUTTON_REVERSE LITERAL1 +BUTTON_UNUSED2 LITERAL1 +BUTTON_SEQUENTIAL LITERAL1 +BUTTON_3 LITERAL1 +BUTTON_2 LITERAL1 +BUTTON_4 LITERAL1 +BUTTON_1 LITERAL1 +BUTTON_NORTH LITERAL1 +BUTTON_EAST LITERAL1 +BUTTON_WEST LITERAL1 +BUTTON_SOUTH LITERAL1 +DPAD_RIGHT LITERAL1 +DPAD_LEFT LITERAL1 +DPAD_DOWN LITERAL1 +DPAD_UP LITERAL1 + +####################################### +# LogitechShifterG27 Methods and Functions (KEYWORD2) +####################################### + +getButton KEYWORD2 +getButtonChanged KEYWORD2 +getDpadAngle KEYWORD2 +buttonsChanged KEYWORD2 +setPowerLED KEYWORD2 +getPowerLED KEYWORD2 + +####################################### +# LogitechShifterG25 Methods and Functions (KEYWORD2) +####################################### + +inSequentialMode KEYWORD2 +getShiftUp KEYWORD2 +getShiftDown KEYWORD2 + +setCalibrationSequential KEYWORD2 +serialCalibrationSequential KEYWORD2 + ####################################### # Handbrake Methods and Functions (KEYWORD2) ####################################### @@ -118,6 +182,9 @@ serialCalibration KEYWORD2 # Constants (LITERAL1) ####################################### +# Unused Pin Flag +UnusedPin LITERAL1 + # Axis Enum X LITERAL1 Y LITERAL1 @@ -128,7 +195,3 @@ Accelerator LITERAL1 Throttle LITERAL1 Brake LITERAL1 Clutch LITERAL1 - -# Board Pin Definitions -SHIFTER_SHIELD_V1_PINS LITERAL1 -PEDAL_SHIELD_V1_PINS LITERAL1 diff --git a/src/SimRacing.cpp b/src/SimRacing.cpp index e3472c4..0129017 100644 --- a/src/SimRacing.cpp +++ b/src/SimRacing.cpp @@ -29,6 +29,87 @@ namespace SimRacing { +#if defined(__AVR_ATmega32U4__) || defined(SIM_RACING_DOXYGEN) + +template<> +LogitechPedals CreateShieldObject() { + // Power (VCC): DE-9 pin 9, bridged to DE-9 pin 6 + // Ground (GND): DE-9 pin 1 + + const PinNum Pin_Gas = A2; // DE-9 pin 2 + const PinNum Pin_Brake = A1; // DE-9 pin 3 + const PinNum Pin_Clutch = A0; // DE-9 pin 4 + const PinNum Pin_Detect = 10; // DE-9 pin 6, requires 10k Ohm pull-down + + return LogitechPedals(Pin_Gas, Pin_Brake, Pin_Clutch, Pin_Detect); +} + +template<> +LogitechPedals CreateShieldObject() { + // version 2 of the pedals shield has the same pinout, + // so we can use the v1 function + return CreateShieldObject(); +} + +template<> +LogitechShifter CreateShieldObject() { + // Power (VCC): DE-9 pin 9, bridged to DE-9 pin 7 + // Ground (GND): DE-9 pin 6 + // DE-9 pin 3 (CS) needs to be pulled-up to VCC + + const PinNum Pin_X_Wiper = A1; // DE-9 pin 4 + const PinNum Pin_Y_Wiper = A0; // DE-9 pin 8 + const PinNum Pin_DataOut = 14; // DE-9 pin 2 + const PinNum Pin_Detect = A2; // DE-9 pin 7, requires 10k Ohm pull-down + + return LogitechShifter(Pin_X_Wiper, Pin_Y_Wiper, Pin_DataOut, Pin_Detect); +} + +template<> +LogitechShifter CreateShieldObject() { + // version 2 of the shifter shield has the same data pinout for + // the Driving Force shifter, so we can use the v1 function + return CreateShieldObject(); +} + +template<> +LogitechShifterG27 CreateShieldObject() { + // Power (VCC): DE-9 pin 9, bridged to DE-9 pin 7 + // Ground (GND): DE-9 pin 6 + + const PinNum Pin_X_Wiper = A1; // DE-9 pin 4 + const PinNum Pin_Y_Wiper = A0; // DE-9 pin 8 + const PinNum Pin_DataOut = 14; // DE-9 pin 2 + + const PinNum Pin_Latch = 10; // DE-9 pin 3, aka chip select, requires 10k Ohm pull-up + const PinNum Pin_Clock = 15; // DE-9 pin 1, should have 470 Ohm resistor to prevent shorts + + const PinNum Pin_LED = 16; // DE-9 pin 5, has a 100-120 Ohm series resistor + const PinNum Pin_Detect = A2; // DE-9 pin 7, requires 10k Ohm pull-down + + return LogitechShifterG27(Pin_X_Wiper, Pin_Y_Wiper, Pin_Latch, Pin_Clock, Pin_DataOut, Pin_LED, Pin_Detect); +} + +template<> +LogitechShifterG25 CreateShieldObject() { + // Power (VCC): DE-9 pin 9, bridged to DE-9 pin 1 + // Ground (GND): DE-9 pin 6 + + const PinNum Pin_X_Wiper = A1; // DE-9 pin 4 + const PinNum Pin_Y_Wiper = A0; // DE-9 pin 8 + const PinNum Pin_DataOut = 14; // DE-9 pin 2 + + const PinNum Pin_Latch = 10; // DE-9 pin 3, aka chip select, requires 10k Ohm pull-up + const PinNum Pin_Clock = A2; // DE-9 pin 7, should have 470 Ohm resistor to prevent shorts + + const PinNum Pin_LED = 16; // DE-9 pin 5, has a 100-120 Ohm series resistor + const PinNum Pin_Detect = 15; // DE-9 pin 1, requires 10k Ohm pull-down + + return LogitechShifterG25(Pin_X_Wiper, Pin_Y_Wiper, Pin_Latch, Pin_Clock, Pin_DataOut, Pin_LED, Pin_Detect); +} +#endif // ATmega32U4 for shield functions + + /** * Take a pin number as an input and sanitize it to a known working value * @@ -176,9 +257,9 @@ static void readFloat(float& value, Stream& client) { // DeviceConnection # //######################################################### -DeviceConnection::DeviceConnection(PinNum pin, bool invert, unsigned long detectTime) +DeviceConnection::DeviceConnection(PinNum pin, bool activeLow, unsigned long detectTime) : - pin(sanitizePin(pin)), inverted(invert), stablePeriod(detectTime), // constants(ish) + pin(sanitizePin(pin)), inverted(activeLow), stablePeriod(detectTime), // constants(ish) /* Assume we're connected on first call */ @@ -345,16 +426,52 @@ void AnalogInput::setCalibration(AnalogInput::Calibration newCal) { this->cal = newCal; } +//######################################################### +// Peripheral # +//######################################################### + +bool Peripheral::update() { + // if the detector exists, poll for state + if (this->detector) { + this->detector->poll(); + } + + // get the connected state from the detector + const bool connected = this->isConnected(); + + // call the derived class update function + return this->updateState(connected); +} + +bool Peripheral::isConnected() const { + // if detector exists, return state + if (this->detector) { + return this->detector->isConnected(); + } + + // otherwise, assume always connected + return true; +} + +void Peripheral::setDetectPtr(DeviceConnection* d) { + this->detector = d; +} + +void Peripheral::setStablePeriod(unsigned long t) { + // if detector exists, set the stable period + if (this->detector) { + this->detector->setStablePeriod(t); + } +} //######################################################### // Pedals # //######################################################### -Pedals::Pedals(AnalogInput* dataPtr, uint8_t nPedals, PinNum detectPin) +Pedals::Pedals(AnalogInput* dataPtr, uint8_t nPedals) : pedalData(dataPtr), NumPedals(nPedals), - detector(detectPin), changed(false) {} @@ -362,26 +479,29 @@ void Pedals::begin() { update(); // set initial pedal position } -bool Pedals::update() { - changed = false; +bool Pedals::updateState(bool connected) { + this->changed = false; - detector.poll(); - if (detector.getState() == DeviceConnection::Connected) { - // if connected, read all pedal positions + // if we're connected, read all pedal positions + if (connected) { for (int i = 0; i < getNumPedals(); ++i) { changed |= pedalData[i].read(); } } - else if (detector.getState() == DeviceConnection::Unplug) { - // on unplug event, zero all pedals + + // otherwise, zero all pedals + else { for (int i = 0; i < getNumPedals(); ++i) { const int min = pedalData[i].getMin(); - pedalData[i].setPosition(min); + const int prev = pedalData[i].getPositionRaw(); + if (min != prev) { + pedalData[i].setPosition(min); + changed = true; + } } - changed = true; // set flag so we know everything moved to 0 } - return changed; + return this->changed; } long Pedals::getPosition(PedalID pedal, long rMin, long rMax) const { @@ -555,8 +675,8 @@ void Pedals::serialCalibration(Stream& iface) { } -TwoPedals::TwoPedals(PinNum gasPin, PinNum brakePin, PinNum detectPin) - : Pedals(pedalData, NumPedals, detectPin), +TwoPedals::TwoPedals(PinNum gasPin, PinNum brakePin) + : Pedals(pedalData, NumPedals), pedalData{ AnalogInput(gasPin), AnalogInput(brakePin) } {} @@ -566,8 +686,8 @@ void TwoPedals::setCalibration(AnalogInput::Calibration gasCal, AnalogInput::Cal } -ThreePedals::ThreePedals(PinNum gasPin, PinNum brakePin, PinNum clutchPin, PinNum detectPin) - : Pedals(pedalData, NumPedals, detectPin), +ThreePedals::ThreePedals(PinNum gasPin, PinNum brakePin, PinNum clutchPin) + : Pedals(pedalData, NumPedals), pedalData{ AnalogInput(gasPin), AnalogInput(brakePin), AnalogInput(clutchPin) } {} @@ -578,10 +698,13 @@ void ThreePedals::setCalibration(AnalogInput::Calibration gasCal, AnalogInput::C } - LogitechPedals::LogitechPedals(PinNum gasPin, PinNum brakePin, PinNum clutchPin, PinNum detectPin) - : ThreePedals(gasPin, brakePin, clutchPin, detectPin) + : + ThreePedals(gasPin, brakePin, clutchPin), + detectObj(detectPin, false) // active high { + this->setDetectPtr(&this->detectObj); + // taken from calibrating my own pedals. the springs are pretty stiff so while // this covers the whole travel range, users may want to back it down for casual // use (esp. for the brake travel) @@ -589,8 +712,11 @@ LogitechPedals::LogitechPedals(PinNum gasPin, PinNum brakePin, PinNum clutchPin, } LogitechDrivingForceGT_Pedals::LogitechDrivingForceGT_Pedals(PinNum gasPin, PinNum brakePin, PinNum detectPin) - : TwoPedals(gasPin, brakePin, detectPin) + : + TwoPedals(gasPin, brakePin), + detectObj(detectPin, false) // active high { + this->setDetectPtr(&this->detectObj); this->setCalibration({ 646, 0 }, { 473, 1023 }); // taken from calibrating my own pedals } @@ -599,9 +725,22 @@ LogitechDrivingForceGT_Pedals::LogitechDrivingForceGT_Pedals(PinNum gasPin, PinN // Shifter # //######################################################### -Shifter::Shifter(int8_t min, int8_t max) - : MinGear(min), MaxGear(max) -{} +Shifter::Shifter(Gear min, Gear max) + : + MinGear(min), MaxGear(max) +{ + this->currentGear = this->previousGear = 0; // neutral +} + +void Shifter::setGear(Gear gear) { + // if gear is out of range, set it to neutral + if (gear < MinGear || gear > MaxGear) { + gear = 0; + } + + this->previousGear = this->currentGear; + this->currentGear = gear; +} char Shifter::getGearChar(int gear) { char c = '?'; @@ -674,18 +813,17 @@ const float AnalogShifter::CalEngagementPoint = 0.70; const float AnalogShifter::CalReleasePoint = 0.50; const float AnalogShifter::CalEdgeOffset = 0.60; -AnalogShifter::AnalogShifter(PinNum pinX, PinNum pinY, PinNum pinRev, PinNum detectPin) - : - /* In initializing the Shifter, the lowest gear is going to be '-1' if a pin - * exists for reverse, otherwise it's going to be '0' (neutral). - */ - Shifter(sanitizePin(pinRev) != UnusedPin ? -1 : 0, 6), +AnalogShifter::AnalogShifter( + Gear gearMin, Gear gearMax, + PinNum pinX, PinNum pinY, PinNum pinRev +) : + Shifter(gearMin, gearMax), /* Two axes, X and Y */ analogAxis{ AnalogInput(pinX), AnalogInput(pinY) }, pinReverse(sanitizePin(pinRev)), - detector(detectPin, false) // not inverted + reverseState(false) {} void AnalogShifter::begin() { @@ -695,48 +833,39 @@ void AnalogShifter::begin() { update(); // set initial gear position } -bool AnalogShifter::update() { - detector.poll(); - - switch (detector.getState()) { - - // connected! poll the ADC for new analog axis data - case(DeviceConnection::Connected): - analogAxis[Axis::X].read(); - analogAxis[Axis::Y].read(); - break; - - // on an unplug event, we want to reset our position back to - // neutral and then immediately return - case(DeviceConnection::Unplug): - { - const int8_t previousGear = this->getGear(); - +bool AnalogShifter::updateState(bool connected) { + // if not connected, reset our position back to neutral + // and immediately return + if (!connected) { + // set axis values to calibrated neutral analogAxis[Axis::X].setPosition(calibration.neutralX); analogAxis[Axis::Y].setPosition(calibration.neutralY); - if (previousGear != 0) changed = true; - currentGear = 0; - return changed; - break; - } + // set reverse state to unpressed + this->reverseState = false; - // if the device is either disconnected or just plugged in and unstable, set gear - // 'changed' to false and then immediately return false to save on processing - case(DeviceConnection::PlugIn): - case(DeviceConnection::Disconnected): - changed = false; - return changed; - break; + // set gear to neutral + this->setGear(0); + + // status changed if gear changed + return this->gearChanged(); } - const int8_t previousGear = this->getGear(); - const bool prevOdd = ((previousGear != -1) && (previousGear & 1)); // were we previously in an odd gear - const bool prevEven = (!prevOdd && previousGear != 0); // were we previously in an even gear - + // poll the analog axes for new data + analogAxis[Axis::X].read(); + analogAxis[Axis::Y].read(); const int x = analogAxis[Axis::X].getPosition(); const int y = analogAxis[Axis::Y].getPosition(); - int8_t newGear = 0; + + // poll the reverse button and cache in the class + this->reverseState = this->readReverseButton(); + + // check previous gears for comparison + const Gear previousGear = this->getGear(); + const bool prevOdd = ((previousGear != -1) && (previousGear & 1)); // were we previously in an odd gear + const bool prevEven = (!prevOdd && previousGear != 0); // were we previously in an even gear + + Gear newGear = 0; // If we're below the 'release' thresholds, we must still be in the previous gear if ((prevOdd && y > calibration.oddRelease) || (prevEven && y < calibration.evenRelease)) { @@ -777,10 +906,10 @@ bool AnalogShifter::update() { } } - changed = (newGear != previousGear) ? 1 : 0; - currentGear = newGear; + // finally, store the newly calculated gear + this->setGear(newGear); - return changed; + return this->gearChanged(); } long AnalogShifter::getPosition(Axis ax, long min, long max) const { @@ -793,15 +922,21 @@ int AnalogShifter::getPositionRaw(Axis ax) const { return analogAxis[ax].getPositionRaw(); } -bool AnalogShifter::getReverseButton() const { - // if the reverse pin is not set *or* if the device is not currently - // connected, avoid reading the floating input and just return 'false' - if (pinReverse == UnusedPin || detector.getState() != DeviceConnection::Connected) { +bool AnalogShifter::readReverseButton() { + // if the reverse pin is not set, avoid reading the + // floating input and just return 'false' + if (pinReverse == UnusedPin) { return false; } return digitalRead(pinReverse); } +bool AnalogShifter::getReverseButton() const { + // return the cached reverse state from updateState(bool) + // do NOT poll the button! + return this->reverseState; +} + void AnalogShifter::setCalibration( GearPosition neutral, GearPosition g1, GearPosition g2, GearPosition g3, GearPosition g4, GearPosition g5, GearPosition g6, @@ -997,19 +1132,543 @@ void AnalogShifter::serialCalibration(Stream& iface) { } LogitechShifter::LogitechShifter(PinNum pinX, PinNum pinY, PinNum pinRev, PinNum detectPin) - : AnalogShifter(pinX, pinY, pinRev, detectPin) + : + AnalogShifter( + -1, 6, // includes reverse and gears 1-6 + pinX, pinY, pinRev + ), + + detectObj(detectPin, false) // active high { + this->setDetectPtr(&this->detectObj); this->setCalibration({ 490, 440 }, { 253, 799 }, { 262, 86 }, { 460, 826 }, { 470, 76 }, { 664, 841 }, { 677, 77 }); } + +LogitechShifterG27::LogitechShifterG27( + PinNum pinX, PinNum pinY, + PinNum pinLatch, PinNum pinClock, PinNum pinData, + PinNum pinLed, + PinNum pinDetect +) : + LogitechShifter(pinX, pinY, UnusedPin, pinDetect), + + pinLatch(sanitizePin(pinLatch)), pinClock(sanitizePin(pinClock)), pinData(sanitizePin(pinData)), + pinLed(sanitizePin(pinLed)) +{ + this->pinModesSet = false; + this->setPowerLED(1); // power LED on by default + this->buttonStates = this->previousButtons = 0x0000; // zero all button data + + // using the calibration values from my own G27 shifter + this->setCalibration({ 453, 470 }, { 247, 828 }, { 258, 6 }, { 449, 878 }, { 472, 5 }, { 645, 880 }, { 651, 21 }); +} + +void LogitechShifterG27::cacheButtons(uint16_t newStates) { + this->previousButtons = this->buttonStates; // save current to previous + this->buttonStates = newStates; // replace current with new value +} + +void LogitechShifterG27::setPinModes(bool enabled) { + // check if pins are valid. if one or more pins is unused, + // this isn't going to work and we shouldn't bother setting + // any of the pin states + if ( + this->pinData == UnusedPin || + this->pinLatch == UnusedPin || + this->pinClock == UnusedPin) + { + return; + } + + // set up data pin to read from regardless + pinMode(this->pinData, INPUT); + + // enabled = drive the output pins + if (enabled) { + // note: writing the output before setting the + // pin mode so that we don't accidentally drive + // the wrong direction momentarily + + // set latch pin as output, HIGH on idle + digitalWrite(this->pinLatch, HIGH); + pinMode(this->pinLatch, OUTPUT); + + // set clock pin as output, LOW on idle + digitalWrite(this->pinClock, LOW); + pinMode(this->pinClock, OUTPUT); + + // if we have an LED pin, set it to output and write the + // commanded state (inverted, as the LED is active-low) + if (this->pinLed != UnusedPin) { + digitalWrite(this->pinLed, !(this->ledState)); + pinMode(this->pinLed, OUTPUT); + } + } + + // disabled = leave output pins as high-z + else { + // note: setting the mode before writing the + // output for the same reason; changing in + // high-z mode is safer + + // set latch pin as high impedance, with pull-up + pinMode(this->pinLatch, INPUT); + digitalWrite(this->pinLatch, HIGH); + + // set clock pin as high impedance, no pull-up + pinMode(this->pinClock, INPUT); + digitalWrite(this->pinClock, LOW); + + // if we have an LED pin, set it to input, LOW on idle + if (this->pinLed != UnusedPin) { + pinMode(this->pinLed, INPUT); + digitalWrite(this->pinLed, LOW); + } + } + + this->pinModesSet = enabled; +} + +void LogitechShifterG27::setPowerLED(bool state) { + this->ledState = state; +} + +uint16_t LogitechShifterG27::readShiftRegisters() { + // if the pin outputs are not set, quit (none pressed) + if (!this->pinModesSet) return 0x0000; + + uint16_t data = 0x0000; + + // pulse shift register latch from high to low to high, 12 us + // (this timing is *completely* arbitrary, but it's nice to have + // *some* delay so that much faster MCUs don't blow through it) + digitalWrite(this->pinLatch, LOW); + delayMicroseconds(12); + digitalWrite(this->pinLatch, HIGH); + delayMicroseconds(12); + + // clock is pulsed from LOW to HIGH on every bit, + // and then left to idle low + for (int i = 0; i < 16; ++i) { + digitalWrite(this->pinClock, LOW); + const bool state = digitalRead(this->pinData); + if (state) data |= 1 << (15 - i); // store data in word, MSB-first + digitalWrite(this->pinClock, HIGH); + delayMicroseconds(6); + } + digitalWrite(this->pinClock, LOW); + + // edge case: two of the bits (0x8000 and 0x2000) are connected only to + // pull-down resistors, and should theoretically never be high. If they, + // and all other bits, *are* high, then we are not reading from a shifter + // that has shift registers. The "Driving Force" (G29/G920/G923) shifter + // has its data output connected to the 'reverse' button through a buffer, + // and will report 'high' if the reverse button is pressed no matter how + // many times the clock is pulsed. + // + // QED: we are connected to a "Driving Force" shifter, and not a G27. + // That's okay! If we set the state of the 'reverse' button and clear + // all others, we can still behave like a G27. + if (data == 0xFFFF) { + data = (1 << (uint8_t) Button::BUTTON_REVERSE); + } + + return data; +} + +void LogitechShifterG27::begin() { + // disable pin outputs. this sets the initial + // 'safe' state. the outputs will be enabled + // by the 'updateState(bool)' function when needed. + this->setPinModes(0); + + // call the begin() class of the base, which will also + // poll 'update()' on our behalf + this->AnalogShifter::begin(); +} + +bool LogitechShifterG27::updateState(bool connected) { + bool changed = false; + + // if we're connected, set the pin modes, read the + // shift registers, and cache the data + if (connected) { + if (!this->pinModesSet) { + this->setPinModes(1); + } + + if (this->pinLed != UnusedPin) { + digitalWrite(this->pinLed, !(this->ledState)); // active low + } + + const uint16_t data = this->readShiftRegisters(); + this->cacheButtons(data); + changed |= this->buttonsChanged(); + } + + // if we're *not* connected, reset the pin modes and + // set no buttons pressed + else { + if (this->pinModesSet) { + this->setPinModes(0); + } + + this->cacheButtons(0x0000); + changed |= this->buttonsChanged(); + } + + // we also need to update the data for the analog shifter + changed |= AnalogShifter::updateState(connected); + + return changed; +} + +bool LogitechShifterG27::buttonsChanged() const { + return this->buttonStates != this->previousButtons; +} + +bool LogitechShifterG27::getButton(Button button) const { + return this->extractButton(button, this->buttonStates); +} + +bool LogitechShifterG27::getButtonChanged(Button button) const { + return this->getButton(button) != this->extractButton(button, this->previousButtons); +} + +int LogitechShifterG27::getDpadAngle() const { + const Button pads[4] = { + DPAD_UP, + DPAD_RIGHT, + DPAD_DOWN, + DPAD_LEFT, + }; + + // combine pads to a bitfield (nybble) + uint8_t dpad = 0x00; + for (uint8_t i = 0; i < 4; ++i) { + dpad |= (this->getButton(pads[i]) << i); + } + + // The hatswitch value is from 0-7 proceeding clockwise + // from top (0 is 'up', 1 is 'up + right', etc.). I don't + // know of a great way to do this, so have this naive + // lookup table with a built-in SOCD cleaner + + // For this, simultaneous opposing cardinal directions + // are neutral (because this is presumably used for + // navigation only, and not fighting games. Probably). + + // bitfield to hatswitch lookup table + const uint8_t hat_table[16] = { + 8, // 0b0000, Unpressed + 0, // 0b0001, Up + 2, // 0b0010, Right + 1, // 0b0011, Right + Up + 4, // 0b0100, Down + 8, // 0b0101, Down + Up (SOCD None) + 3, // 0b0110, Down + Right + 2, // 0b0111, Down + Right + Up (SOCD Right) + 6, // 0b1000, Left + 7, // 0b1001, Left + Up + 8, // 0b1010, Left + Right (SOCD None) + 0, // 0b1011, Left + Right + Up (SOCD Up) + 5, // 0b1100, Left + Down + 6, // 0b1101, Left + Down + Up (SOCD Left) + 4, // 0b1110, Left + Down + Right (SOCD Down) + 8, // 0b1111, Left + Down + Right + Up (SOCD None) + }; + + // multiply the 0-8 value by 45 to get it in degrees + int16_t angle = hat_table[dpad & 0x0F] * 45; + + // edge case: if no buttons are pressed, the angle is '-1' + if (angle == 360) angle = -1; + + return angle; +} + +bool LogitechShifterG27::readReverseButton() { + // this virtual function is provided for the sake of the AnalogShifter base + // class, which can use this to get the button state from the shift register + // without needing to interface with the shift registers themselves + return this->getButton(BUTTON_REVERSE); +} + + +/* +* Static calibration constants +* These values are arbitrary - just what worked well with my own shifter. +*/ +const float LogitechShifterG25::CalEngagementPoint = 0.70; +const float LogitechShifterG25::CalReleasePoint = 0.50; + +LogitechShifterG25::LogitechShifterG25( + PinNum pinX, PinNum pinY, + PinNum pinLatch, PinNum pinClock, PinNum pinData, + PinNum pinLed, + PinNum pinDetect +) : + LogitechShifterG27( + pinX, pinY, + pinLatch, pinClock, pinData, + pinLed, + pinDetect + ), + + sequentialProcess(false), // not in sequential mode + sequentialState(0) // no sequential buttons pressed +{ + // using the calibration values from my own G25 shifter + this->setCalibration({ 508, 435 }, { 310, 843 }, { 303, 8 }, { 516, 827 }, { 540, 14 }, { 713, 846 }, { 704, 17 }); + this->setCalibrationSequential(425, 619, 257); +} + +void LogitechShifterG25::begin() { + this->sequentialProcess = false; // clear process flag + this->sequentialState = 0; // clear any pressed buttons + + this->LogitechShifterG27::begin(); // call base class begin() +} + +bool LogitechShifterG25::updateState(bool connected) { + // call the base class to update the state of the + // buttons and the H-pattern shifter + bool changed = this->LogitechShifterG27::updateState(connected); + + // if we're connected and in sequential mode... + if (connected && this->inSequentialMode()) { + + // clear 'changed', because this will falsely report a change + // if we've "shifted" into 2nd/4th in the process of sequential + // shifting + changed = false; + + // force neutral gear, ignoring the H-pattern selection + this->setGear(0); + + // edge case: if we've not just switched into sequential mode, + // we need to ignore the H-pattern gear change (to 2/4, and then + // set by us to neutral). We can do that, hackily, by setting to + // neutral again to clear the cached gear for comparison. + if (this->sequentialProcess) { + this->setGear(0); + } + + // read the raw y axis value, ignoring the H-pattern calibration + const int y = this->getPositionRaw(Axis::Y); + + // save the previous state for reference + const int8_t prevState = this->sequentialState; + + // if we're neutral, check for up/down shift + if (this->sequentialState == 0) { + if (y >= this->seqCalibration.upTrigger) this->sequentialState = 1; + else if (y <= this->seqCalibration.downTrigger) this->sequentialState = -1; + } + + // if we're in up-shift mode, check for release + else if ((this->sequentialState == 1) && (y < this->seqCalibration.upRelease)) { + this->sequentialState = 0; + } + + // if we're in down-shift mode, check for release + else if ((this->sequentialState == -1) && (y > this->seqCalibration.downRelease)) { + this->sequentialState = 0; + } + + // set the 'changed' flag if the sequential state changed + if (prevState != this->sequentialState) { + changed = true; + } + // otherwise, set 'changed' based on the buttons *only* + else { + changed = this->buttonsChanged(); + } + + // set 'process' flag to handle edge case on subsequent updates + this->sequentialProcess = true; + } + + // if we're not connected or if the sequential mode has been disabled, + // clear the sequential flags if they have been set + else { + if (this->sequentialProcess) { + this->sequentialProcess = false; // not in sequential mode + this->sequentialState = 0; // no sequential buttons pressed + changed = true; + } + } + + return changed; +} + +bool LogitechShifterG25::inSequentialMode() const { + return this->getButton(BUTTON_SEQUENTIAL); +} + +bool LogitechShifterG25::getShiftUp() const { + return this->sequentialState == 1; +} + +bool LogitechShifterG25::getShiftDown() const { + return this->sequentialState == -1; +} + +void LogitechShifterG25::setCalibrationSequential(int neutral, int up, int down, float engagePoint, float releasePoint) { + // limit percentage thresholds + engagePoint = floatPercent(engagePoint); + releasePoint = floatPercent(releasePoint); + + // prevent release point from being higher than engage + // (which will prevent the shifter from working at all) + if (releasePoint > engagePoint) { + releasePoint = engagePoint; + } + + // calculate ranges + const int upRange = up - neutral; + const int downRange = neutral - down; + + // calculate calibration points + this->seqCalibration.upTrigger = neutral + (upRange * engagePoint); + this->seqCalibration.upRelease = neutral + (upRange * releasePoint); + + this->seqCalibration.downTrigger = neutral - (downRange * engagePoint); + this->seqCalibration.downRelease = neutral - (downRange * releasePoint); +} + +void LogitechShifterG25::serialCalibrationSequential(Stream& iface) { + // err if not connected + if (this->isConnected() == false) { + iface.print(F("Error! Cannot perform calibration, ")); + iface.print(F("shifter")); + iface.println(F(" is not connected.")); + return; + } + + const char* separator = "------------------------------------"; + + iface.println(); + iface.println(F("Sim Racing Library G25 Sequential Shifter Calibration")); + iface.println(separator); + iface.println(); + + while (this->inSequentialMode() == false) { + iface.print(F("Please press down on the shifter and move the dial counter-clockwise to put the shifter into sequential mode")); + iface.print(F(". Send any character to continue.")); + iface.println(F(" Send 'q' to quit.")); + iface.println(); + + waitClient(iface); + this->update(); + + // quit if user sends 'q' + if (iface.read() == 'q') { + iface.println(F("Quitting sequential calibration! Goodbye <3")); + iface.println(); + return; + } + + // send an error if we're still not there + if (this->inSequentialMode() == false) { + iface.println(F("Error: The shifter is not in sequential mode")); + iface.println(); + } + } + + float engagementPoint = LogitechShifterG25::CalEngagementPoint; + float releasePoint = LogitechShifterG25::CalReleasePoint; + + const uint8_t NumPoints = 3; + const char* directions[2] = { + "up", + "down", + }; + int data[NumPoints]; + + int& neutral = data[0]; + int& yMax = data[1]; + int& yMin = data[2]; + + for (uint8_t i = 0; i < NumPoints; ++i) { + if (i == 0) { + iface.print(F("Leave the gear shifter in neutral")); + } + else { + iface.print(F("Please move the gear shifter to sequentially shift ")); + iface.print(directions[i - 1]); + iface.print(F(" and hold it there")); + } + iface.println(F(". Send any character to continue.")); + waitClient(iface); + + this->update(); + data[i] = this->getPositionRaw(Axis::Y); + iface.println(); // spacing + } + + iface.println(F("These settings are optional. Send 'y' to customize. Send any other character to continue with the default values.")); + + iface.print(F(" * Shift Engagement Point: \t")); + iface.println(engagementPoint); + + iface.print(F(" * Shift Release Point: \t")); + iface.println(releasePoint); + + iface.println(); + + waitClient(iface); + + if (iface.read() == 'y') { + iface.println(F("Set the engagement point as a floating point percentage. This is the percentage away from the neutral axis on Y to start shifting.")); + readFloat(engagementPoint, iface); + iface.println(); + + iface.println(F("Set the release point as a floating point percentage. This is the percentage away from the neutral axis on Y to stop shifting. It must be less than the engagement point.")); + readFloat(releasePoint, iface); + iface.println(); + } + + flushClient(iface); + + // apply and print + this->setCalibrationSequential(neutral, yMax, yMin, engagementPoint, releasePoint); + + iface.println(F("Here is your calibration:")); + iface.println(separator); + iface.println(); + + iface.print(F("shifter.setCalibrationSequential( ")); + + iface.print(neutral); + iface.print(", "); + iface.print(yMax); + iface.print(", "); + iface.print(yMin); + iface.print(", "); + + iface.print(engagementPoint); + iface.print(", "); + iface.print(releasePoint); + iface.print(");"); + iface.println(); + + iface.println(); + iface.println(separator); + iface.println(); + + iface.println(F("Paste this line into the setup() function to calibrate on startup.")); + iface.println(F("\n\nCalibration complete! :)\n")); +} + //######################################################### // Handbrake # //######################################################### -Handbrake::Handbrake(PinNum pinAx, PinNum detectPin) +Handbrake::Handbrake(PinNum pinAx) : analogAxis(pinAx), - detector(detectPin), changed(false) {} @@ -1017,19 +1676,26 @@ void Handbrake::begin() { update(); // set initial handbrake position } -bool Handbrake::update() { - changed = false; +bool Handbrake::updateState(bool connected) { + this->changed = false; - detector.poll(); - if (detector.getState() == DeviceConnection::Connected) { - changed = analogAxis.read(); + // if connected, read state of the axis + if (connected) { + this->changed = this->analogAxis.read(); } - else if (detector.getState() == DeviceConnection::Unplug) { - analogAxis.setPosition(analogAxis.getMin()); - changed = true; + + // otherwise, set axis to its minimum (idle) position + else { + const int min = this->analogAxis.getMin(); + const int prev = this->analogAxis.getPositionRaw(); + + if (min != prev) { + this->analogAxis.setPosition(min); + this->changed = true; + } } - return changed; + return this->changed; } long Handbrake::getPosition(long rMin, long rMax) const { diff --git a/src/SimRacing.h b/src/SimRacing.h index b1cea20..fd5a780 100644 --- a/src/SimRacing.h +++ b/src/SimRacing.h @@ -75,12 +75,13 @@ namespace SimRacing { /** * Class constructor * - * @param pin the pin number being read. Can be 'UnusedPin' to disable. - * @param invert whether the input is inverted, so 'LOW' is detected instead of 'HIGH' + * @param pin the pin number being read. Can be 'UnusedPin' to disable. + * @param activeLow whether the device is detected on a high signal (false, + * default) or a low signal (true) * @param detectTime the amount of time, in ms, the input must be stable for - * before it's interpreted as 'detected' + * before it's interpreted as 'detected' */ - DeviceConnection(PinNum pin, bool invert = false, unsigned long detectTime = 250); + DeviceConnection(PinNum pin, bool activeLow = false, unsigned long detectTime = 250); /** * Checks if the pin detects a connection. This polls the input and checks @@ -105,7 +106,8 @@ namespace SimRacing { bool isConnected() const; /** - * Allows the user to change the stable period of the detector. + * Set how long the detection pin must be stable for before the device + * is considered to be 'connected' * * @param t the amount of time, in ms, the input must be stable for * (no changes) before it's interpreted as 'detected' @@ -248,6 +250,11 @@ namespace SimRacing { */ class Peripheral { public: + /** + * Class destructor + */ + virtual ~Peripheral() {} + /** * Initialize the hardware (if necessary) */ @@ -256,12 +263,52 @@ namespace SimRacing { /** * Perform a poll of the hardware to refresh the class state * - * @return 'true' if device state changed, 'false' otherwise + * @returns 'true' if device state changed, 'false' otherwise + */ + bool update(); + + /** + * Check if the device is physically connected to the board. That means + * it is both present and detected long enough to be considered 'stable'. + * + * @returns 'true' if the device is connected, 'false' otherwise + */ + bool isConnected() const; + + /** @copydoc DeviceConnection::setStablePeriod(unsigned long) */ + void setStablePeriod(unsigned long t); + + protected: + /** + * Perform an internal poll of the hardware to refresh the class state + * + * This function is called from within the public update() in order to + * refresh the cached state of the peripheral. It needs to be defined + * in every derived class. This function is the *only* place where the + * cached device state should be changed. + * + * @param connected the state of the device connection + * + * @returns 'true' if device state changed, 'false' otherwise + */ + virtual bool updateState(bool connected) = 0; + + /** + * Sets the pointer to the detector object + * + * The detector object is used to check if the peripheral is connected + * to the microcontroller. The object is polled on every update. + * + * Although the detector instance is accessed via the Peripheral class, + * it is the responsibility of the dervied class to store the + * DeviceConnection object and manage its lifetime. + * + * @param d pointer to the detector object */ - virtual bool update() = 0; + void setDetectPtr(DeviceConnection* d); - /** @copydoc DeviceConnection::isConnected() */ - virtual bool isConnected() const { return true; } + private: + DeviceConnection* detector; ///< Pointer to a device connection instance }; @@ -293,18 +340,17 @@ namespace SimRacing { /** * Class constructor * - * @param dataPtr pointer to the analog input data managed by the class, stored elsewhere - * @param nPedals the number of pedals stored in said data pointer - * @param detectPin the digital pin for device detection (high is detected) + * @param dataPtr pointer to the analog input data managed by the class, + * stored elsewhere + * @param nPedals the number of pedals stored in said data pointer */ - Pedals(AnalogInput* dataPtr, uint8_t nPedals, PinNum detectPin); + Pedals( + AnalogInput* dataPtr, uint8_t nPedals + ); /** @copydoc Peripheral::begin() */ virtual void begin(); - /** @copydoc Peripheral::update() */ - virtual bool update(); - /** * Retrieves the buffered position for the pedal, rescaled to a * nominal range using the calibration values. @@ -366,9 +412,6 @@ namespace SimRacing { */ void serialCalibration(Stream& iface = Serial); - /** @copydoc Peripheral::isConnected() */ - bool isConnected() const { return detector.isConnected(); } - /** * Utility function to get the string name for each pedal. * @@ -377,10 +420,13 @@ namespace SimRacing { */ static String getPedalName(PedalID pedal); + protected: + /** @copydoc Peripheral::updateState(bool) */ + virtual bool updateState(bool connected); + private: AnalogInput* pedalData; ///< pointer to the pedal data const int NumPedals; ///< number of pedals managed by this class - DeviceConnection detector; ///< detector instance for checking if the pedals are connected bool changed; ///< whether the pedal position has changed since the previous update }; @@ -393,11 +439,12 @@ namespace SimRacing { /** * Class constructor * - * @param pinGas the analog pin for the gas pedal potentiometer - * @param pinBrake the analog pin for the brake pedal potentiometer - * @param pinDetect the digital pin for device detection (high is detected) + * @param pinGas the analog pin for the gas pedal potentiometer + * @param pinBrake the analog pin for the brake pedal potentiometer */ - TwoPedals(PinNum pinGas, PinNum pinBrake, PinNum pinDetect = UnusedPin); + TwoPedals( + PinNum pinGas, PinNum pinBrake + ); /** * Sets the calibration data (min/max) for the pedals @@ -424,9 +471,10 @@ namespace SimRacing { * @param pinGas the analog pin for the gas pedal potentiometer * @param pinBrake the analog pin for the brake pedal potentiometer * @param pinClutch the analog pin for the clutch pedal potentiometer - * @param pinDetect the digital pin for device detection (high is detected) */ - ThreePedals(PinNum pinGas, PinNum pinBrake, PinNum pinClutch, PinNum pinDetect = UnusedPin); + ThreePedals( + PinNum pinGas, PinNum pinBrake, PinNum pinClutch + ); /** * Sets the calibration data (min/max) for the pedals @@ -456,13 +504,18 @@ namespace SimRacing { */ class Shifter : public Peripheral { public: + /** + * Type alias for gear numbers + */ + using Gear = int8_t; + /** * Class constructor * * @param min the lowest gear possible * @param max the highest gear possible */ - Shifter(int8_t min, int8_t max); + Shifter(Gear min, Gear max); /** * Returns the currently selected gear. @@ -472,7 +525,7 @@ namespace SimRacing { * * @return current gear index */ - int8_t getGear() const { return currentGear; } + Gear getGear() const { return currentGear; } /** * Returns a character that represents the given gear. @@ -516,28 +569,42 @@ namespace SimRacing { * * @return 'true' if gear has changed, 'false' otherwise */ - bool gearChanged() const { return changed; } + bool gearChanged() const { + return this->currentGear != this->previousGear; + } /** * Retrieves the minimum possible gear index. * * @return the lowest gear index */ - int8_t getGearMin() { return MinGear; } + Gear getGearMin() { return MinGear; } /** * Retrieves the maximum possible gear index. * * @return the highest gear index */ - int8_t getGearMax() { return MaxGear; } + Gear getGearMax() { return MaxGear; } protected: - const int8_t MinGear; ///< the lowest selectable gear - const int8_t MaxGear; ///< the highest selectable gear + /** + * Changes the currently set gear, internally + * + * This function sanitizes the newly selected gear with MinGear / MaxGear, + * and handles caching the previous value for checking if the gear has + * changed. + * + * @param gear the new gear value to set + */ + void setGear(Gear gear); + + private: + const Gear MinGear; ///< the lowest selectable gear + const Gear MaxGear; ///< the highest selectable gear - int8_t currentGear; ///< index of the current gear - bool changed; ///< whether the gear has changed since the previous update + Gear currentGear; ///< index of the current gear + Gear previousGear; ///< index of the last selected gear }; @@ -549,12 +616,23 @@ namespace SimRacing { /** * Class constructor * - * @param pinX the analog input pin for the X axis - * @param pinY the analog input pin for the Y axis - * @param pinRev the digital input pin for the 'reverse' button - * @param pinDetect the digital pin for device detection (high is detected) + * @param gearMin the lowest gear possible + * @param gearMax the highest gear possible + * @param pinX the analog input pin for the X axis + * @param pinY the analog input pin for the Y axis + * @param pinRev the digital input pin for the 'reverse' button + * + * @note With the way the class is designed, the lowest possible gear is + * -1 (reverse), and the highest possible gear is 6. Setting the + * arguments lower/higher than this will have no effect. Setting + * the arguments within this range will limit to those gears, + * and selecting gears out of range will result in neutral. */ - AnalogShifter(PinNum pinX, PinNum pinY, PinNum pinRev = UnusedPin, PinNum pinDetect = UnusedPin); + AnalogShifter( + Gear gearMin, Gear gearMax, + PinNum pinX, PinNum pinY, + PinNum pinRev = UnusedPin + ); /** * Initializes the hardware pins for reading the gear states. @@ -563,13 +641,6 @@ namespace SimRacing { */ virtual void begin(); - /** - * Polls the hardware to update the current gear state. - * - * @return 'true' if the gear has changed, 'false' otherwise - */ - virtual bool update(); - /** @copydoc AnalogInput::getPosition() * @param ax the axis to get the position of */ @@ -637,10 +708,22 @@ namespace SimRacing { */ void serialCalibration(Stream& iface = Serial); - /** @copydoc Peripheral::isConnected() */ - bool isConnected() const { return detector.isConnected(); } + protected: + /** @copydoc Peripheral::updateState(bool) */ + virtual bool updateState(bool connected); private: + /** + * Read the state of the reverse button + * + * This function should *only* be called as part of updateState(bool), + * to update the state of the device. + * + * @returns the state of the reverse button, 'true' if pressed, + * 'false' otherwise + */ + virtual bool readReverseButton(); + /** * Distance from neutral on Y to register a gear as * being engaged (as a percentage of distance from @@ -676,7 +759,7 @@ namespace SimRacing { AnalogInput analogAxis[2]; ///< Axis data for X and Y PinNum pinReverse; ///< The pin for the reverse gear button - DeviceConnection detector; ///< detector instance for checking if the shifter is connected + bool reverseState; ///< Buffered value for the state of the reverse gear button }; /// @} Shifters @@ -691,22 +774,14 @@ namespace SimRacing { * Class constructor * * @param pinAx analog pin number for the handbrake axis - * @param pinDetect the digital pin for device detection (high is detected) */ - Handbrake(PinNum pinAx, PinNum pinDetect = UnusedPin); + Handbrake(PinNum pinAx); /** * Initializes the pin for reading from the handbrake. */ virtual void begin(); - /** - * Polls the handbrake to update its position. - * - * @return 'true' if the gear has changed, 'false' otherwise - */ - virtual bool update(); - /** * Retrieves the buffered position for the handbrake axis, rescaled to a * nominal range using the calibration values. @@ -733,7 +808,7 @@ namespace SimRacing { * * @return 'true' if the position has changed, 'false' otherwise */ - bool positionChanged() const { return changed; } + bool positionChanged() const { return this->changed; } /// @copydoc AnalogInput::setCalibration() void setCalibration(AnalogInput::Calibration newCal); @@ -741,13 +816,13 @@ namespace SimRacing { /// @copydoc AnalogShifter::serialCalibration() void serialCalibration(Stream& iface = Serial); - /** @copydoc Peripheral::isConnected() */ - bool isConnected() const { return detector.isConnected(); } + protected: + /** @copydoc Peripheral::updateState(bool) */ + virtual bool updateState(bool connected); private: - AnalogInput analogAxis; ///< axis data for the handbrake's position - DeviceConnection detector; ///< detector instance for checking if the handbrake is connected - bool changed; ///< whether the handbrake position has changed since the previous update + AnalogInput analogAxis; ///< axis data for the handbrake's position + bool changed; ///< whether the handbrake position has changed since the previous update }; @@ -759,8 +834,19 @@ namespace SimRacing { */ class LogitechPedals : public ThreePedals { public: - /** @copydoc ThreePedals::ThreePedals */ + /** + * Class constructor + * + * @param pinGas the analog pin for the gas pedal potentiometer, DE-9 pin 2 + * @param pinBrake the analog pin for the brake pedal potentiometer, DE-9 pin 3 + * @param pinClutch the analog pin for the clutch pedal potentiometer, DE-9 pin 4 + * @param pinDetect the digital pin for device detection, DE-9 pin 6. Requires a + * pull-down resistor. + */ LogitechPedals(PinNum pinGas, PinNum pinBrake, PinNum pinClutch, PinNum pinDetect = UnusedPin); + + private: + DeviceConnection detectObj; ///< detector instance for checking if the pedals are connected }; /** @@ -774,8 +860,18 @@ namespace SimRacing { */ class LogitechDrivingForceGT_Pedals : public TwoPedals { public: - /** @copydoc TwoPedals::TwoPedals */ + /** + * Class constructor + * + * @param pinGas the analog pin for the gas pedal potentiometer, DE-9 pin 2 + * @param pinBrake the analog pin for the brake pedal potentiometer, DE-9 pin 3 + * @param pinDetect the digital pin for device detection, DE-9 pin 4. Requires a + * pull-down resistor. + */ LogitechDrivingForceGT_Pedals(PinNum pinGas, PinNum pinBrake, PinNum pinDetect = UnusedPin); + + private: + DeviceConnection detectObj; ///< detector instance for checking if the pedals are connected }; /** @@ -786,47 +882,425 @@ namespace SimRacing { */ class LogitechShifter : public AnalogShifter { public: - /** @copydoc AnalogShifter::AnalogShifter */ + /** + * Class constructor + * + * @param pinX the analog input pin for the X axis, DE-9 pin 4 + * @param pinY the analog input pin for the Y axis, DE-9 pin 8 + * @param pinRev the digital input pin for the 'reverse' button, DE-9 pin 2 + * @param pinDetect the digital pin for device detection, DE-9 pin 7. Requires + * a pull-down resistor. + * + * @note In order to get the 'reverse' signal from the shifter, the chip select + * pin (DE-9 pin 3) needs to be pulled up to VCC. + */ LogitechShifter(PinNum pinX, PinNum pinY, PinNum pinRev = UnusedPin, PinNum pinDetect = UnusedPin); + + private: + DeviceConnection detectObj; ///< detector instance for checking if the shifter is connected }; + /** + * @brief Interface with the Logitech G923 shifter + * @ingroup Shifters + * + * @see https://www.logitechg.com/en-us/products/driving/g923-trueforce-sim-racing-wheel.html + */ + using LogitechShifterG923 = LogitechShifter; -#if defined(__AVR_ATmega32U4__) || defined(SIM_RACING_DOXYGEN) /** - * Pin definitions for the Parts Not Included Logitech Shifter Shield, - * designed for the SparkFun Pro Micro: - * - * * X Wiper: A1 - * * Y Wiper: A0 - * * Reverse Pin: 14 - * * Detect Pin: A2 - * - * This macro can be inserted directly into the constructor in place of the - * normal pin definitions: - * - * @code{.cpp} - * SimRacing::LogitechShifter shifter(SHIFTER_SHIELD_V1_PINS); - * @endcode + * @brief Interface with the Logitech G29 shifter + * @ingroup Shifters + * + * @see https://en.wikipedia.org/wiki/Logitech_G29 */ - #define SHIFTER_SHIELD_V1_PINS A1, A0, 14, A2 + using LogitechShifterG29 = LogitechShifter; /** - * Pin definitions for the Parts Not Included Logitech Pedals Shield, - * designed for the SparkFun Pro Micro: + * @brief Interface with the Logitech G920 shifter + * @ingroup Shifters * - * * Gas Wiper: A2 - * * Brake Wiper: A1 - * * Clutch Wiper: A0 - * * Detect Pin: 10 + * @see https://en.wikipedia.org/wiki/Logitech_G29 + */ + using LogitechShifterG920 = LogitechShifter; + + /** + * @brief Interface with the Logitech G27 shifter + * @ingroup Shifters * - * This macro can be inserted directly into the constructor in place of the - * normal pin definitions: + * The G27 shifter includes the same analog shifter as the Logitech Driving + * Force shifter (implemented in the LogitechShifter class), as well as + * a directional pad and eight buttons. + * + * @see https://en.wikipedia.org/wiki/Logitech_G27 + */ + class LogitechShifterG27 : public LogitechShifter { + public: + /** + * @brief Enumeration of button values + * + * Buttons 1-4 are the red buttons, from left to right. The directional + * pad is read as four separate buttons. The black buttons use cardinal + * directions. + * + * These values represent the bit offset from LSB. Data is read in + * MSB first. + */ + enum Button : uint8_t { + BUTTON_UNUSED1 = 15, ///< Unused shift register pin + BUTTON_REVERSE = 14, ///< Reverse button (press down on the shifter) + BUTTON_UNUSED2 = 13, ///< Unused shift register pin + BUTTON_SEQUENTIAL = 12, ///< Sequential mode button (turn the dial counter-clockwise) + BUTTON_3 = 11, ///< 3rd red button (mid right) + BUTTON_2 = 10, ///< 2nd red button (mid left) + BUTTON_4 = 9, ///< 4th red button (far right) + BUTTON_1 = 8, ///< 1st red button (far left) + BUTTON_NORTH = 7, ///< The top black button + BUTTON_EAST = 6, ///< The right black button + BUTTON_WEST = 5, ///< The left black button + BUTTON_SOUTH = 4, ///< The bottom black button + DPAD_RIGHT = 3, ///< Right button of the directional pad + DPAD_LEFT = 2, ///< Left button of the directional pad + DPAD_DOWN = 1, ///< Down button of the directional pad + DPAD_UP = 0, ///< Top button of the directional pad + }; + + /** + * Class constructor + * + * @param pinX analog input pin for the X axis, DE-9 pin 4 + * @param pinY analog input pin for the Y axis, DE-9 pin 8 + * @param pinLatch digital output pin to pulse to latch data, DE-9 pin 3 + * @param pinClock digital output pin to pulse as a clock, DE-9 pin 1 + * @param pinData digital input pin to use for reading data, DE-9 pin 2 + * @param pinLed digital output pin to light the power LED on connection, + * DE-9 pin 5 + * @param pinDetect digital input pin for device detection, DE-9 pin 7. + * Requires a pull-down resistor. + */ + LogitechShifterG27( + PinNum pinX, PinNum pinY, + PinNum pinLatch, PinNum pinClock, PinNum pinData, + PinNum pinLed = UnusedPin, + PinNum pinDetect = UnusedPin + ); + + /** + * Initializes the hardware pins for reading the gear states and + * the buttons. + */ + virtual void begin(); + + /** + * Retrieve the state of a single button + * + * @param button The button to retrieve + * @returns The state of the button + */ + bool getButton(Button button) const; + + /** + * Checks whether a button has changed between updates + * + * @param button The button to check + * @returns 'true' if the button's state has changed, 'false' otherwise + */ + bool getButtonChanged(Button button) const; + + /** + * Get the directional pad angle in degrees + * + * This is useful for using the directional pad as a "hatswitch", in USB + * + * @returns the directional pad value in degrees (0-360), or '-1' if no + * directional pad buttons are pressed + */ + int getDpadAngle() const; + + /** + * Checks if any of the buttons have changed since the last update + * + * @returns 'true' if any buttons have changed state, 'false' otherwise + */ + bool buttonsChanged() const; + + /** + * Sets the state of the shifter's power LED + * + * If the shifter is currently connected, this function will turn the + * power LED on and off. If the shifter is not connected, this will + * buffer the commanded state and set the LED when the shifter is next + * connected. + * + * @note The update() function must be called in order to push the + * commanded state to the shifter. + * + * @param state the state to set: 1 = on, 0 = off + */ + void setPowerLED(bool state); + + /** + * Gets the commanded state of the shifter's power LED + * + * @returns 'true' if the power LED is commanded to be on, 'false' + * if it's commanded to be off. + */ + bool getPowerLED() const { return this->ledState; } + + protected: + /** @copydoc Peripheral::updateState(bool) */ + virtual bool updateState(bool connected); + + private: + /** + * Extracts a button value from a given data word + * + * @param button The button to extract state for + * @param data Packed data word containing button states + * + * @returns The state of the button + */ + static bool extractButton(Button button, uint16_t data) { + // convert button to single bit with offset, and perform + // a bitwise 'AND' to get the bit value + return data & (1 << (uint8_t) button); + } + + /** + * Store the current button data for reference and replace it with + * a new value + * + * @param newStates The new button states to store + */ + void cacheButtons(uint16_t newStates); + + /** + * Set the pin modes for all pins + * + * @param enabled 'true' to set the pins to their active configuration, + * 'false' to set them to idle / safe + */ + void setPinModes(bool enabled); + + /** + * Shift the button data out from the shift register + * + * @returns the 16-bit data from the shift registers + */ + uint16_t readShiftRegisters(); + + /** @copydoc AnalogShifter::readReverseButton() */ + virtual bool readReverseButton(); + + // Pins for the shift register interface + PinNum pinLatch; ///< Pin to pulse to latch data, DE-9 pin 3 + PinNum pinClock; ///< Pin to pulse as a clock, DE-9 pin 1 + PinNum pinData; ///< Pin to use for reading data, DE-9 pin 2 + + // Generic I/O pins + PinNum pinLed; ///< Pin to light the power LED, DE-9 pin 5 + + // I/O state + bool pinModesSet; ///< Flag for whether the output pins are enabled / driven + bool ledState; ///< Commanded state of the power LED output, DE-9 pin 5 + + // Button states + uint16_t buttonStates; ///< the state of the buttons, as a packed word (where 0 = unpressed and 1 = pressed) + uint16_t previousButtons; ///< the previous state of the buttons, for comparison + }; + + /** + * @brief Interface with the Logitech G25 shifter + * @ingroup Shifters + * + * The G25 shifter includes the same analog shifter as the Logitech Driving + * Force shifter (implemented in the LogitechShifter class), the buttons + * included with the G27 shifter (implemented in the LogitechShifterG27 + * class), and a mode switch between H-pattern and sequential shift modes. + * + * @see https://en.wikipedia.org/wiki/Logitech_G25 + */ + class LogitechShifterG25 : public LogitechShifterG27 { + public: + /** + * Class constructor + * + * @param pinX analog input pin for the X axis, DE-9 pin 4 + * @param pinY analog input pin for the Y axis, DE-9 pin 8 + * @param pinLatch digital output pin to pulse to latch data, DE-9 pin 3 + * @param pinClock digital output pin to pulse as a clock, DE-9 pin 7 + * @param pinData digital input pin to use for reading data, DE-9 pin 2 + * @param pinLed digital output pin to light the power LED on connection, + * DE-9 pin 5 + * @param pinDetect digital input pin for device detection, DE-9 pin 1. + * Requires a pull-down resistor. + */ + LogitechShifterG25( + PinNum pinX, PinNum pinY, + PinNum pinLatch, PinNum pinClock, PinNum pinData, + PinNum pinLed = UnusedPin, + PinNum pinDetect = UnusedPin + ); + + /** @copydoc LogitechShifterG27::begin() */ + virtual void begin(); + + /** + * Check if the shifter is in sequential shifting mode + * + * @returns 'true' if the shifter is in sequential shifting mode, + * 'false' if the shifter is in H-pattern shifting mode. + */ + bool inSequentialMode() const; + + /** + * Check if the sequential shifter is shifted up + * + * @returns 'true' if the sequential shifter is shifted up, + * 'false' otherwise + */ + bool getShiftUp() const; + + /** + * Check if the sequential shifter is shifted down + * + * @returns 'true' if the sequential shifter is shifted down, + * 'false' otherwise + */ + bool getShiftDown() const; + + /** + * Calibrate the sequential shifter for more accurate shifting. + * + * @param neutral the Y position of the shifter in neutral + * @param up the Y position of the shifter in sequential 'up' + * @param down the Y position of the shifter in sequential 'down' + * @param engagePoint distance from neutral on Y to register a gear as + * being engaged (as a percentage of distance from + * neutral to Y max, 0-1) + * @param releasePoint distance from neutral on Y to go back into neutral + * from an engaged gear (as a percentage of distance + * from neutral to Y max, 0-1) + */ + void setCalibrationSequential(int neutral, int up, int down, + float engagePoint = LogitechShifterG25::CalEngagementPoint, + float releasePoint = LogitechShifterG25::CalReleasePoint + ); + + /** @copydoc AnalogShifter::serialCalibration(Stream&) */ + void serialCalibrationSequential(Stream& iface = Serial); + + protected: + /** @copydoc Peripheral::updateState(bool) */ + virtual bool updateState(bool connected); + + private: + /** + * Distance from neutral on Y to register a gear as + * being engaged (as a percentage of distance from + * neutral to Y max, 0-1). Used for calibration. + */ + static const float CalEngagementPoint; + + /** + * Distance from neutral on Y to go back into neutral + * from an engaged gear (as a percentage of distance + * from neutral to Y max, 0-1). Used for calibration. + */ + static const float CalReleasePoint; + + bool sequentialProcess; ///< Flag to indicate whether we are processing sequential shifts + int8_t sequentialState; ///< Tri-state flag for the shift direction. 1 (Up), 0 (Neutral), -1 (Down). + + /*** Internal calibration struct */ + struct SequentialCalibration { + int upTrigger; ///< Threshold to set the sequential shift as 'up' + int upRelease; ///< Threshold to clear the 'up' sequential shift + int downTrigger; ///< Threshold to set the sequential shift as 'down' + int downRelease; ///< Threshold to clear the 'down' sequential shift + } seqCalibration; + }; + + +#if defined(__AVR_ATmega32U4__) || defined(SIM_RACING_DOXYGEN) + /** + * Create an object for use with one of the Sim Racing Shields, designed + * for the SparkFun Pro Micro (32U4). + * + * This is a convenience function, so that users with a shield don't need to + * look up or remember the pin assignments for their hardware. * * @code{.cpp} - * SimRacing::LogitechPedals pedals(PEDAL_SHIELD_V1_PINS); + * // Generic Usage + * auto myObject = SimRacing::CreateShieldObject(); + * + * // Creating a LogitechShifter object for the v2 shifter shield + * auto myShifter = SimRacing::CreateShieldObject(); * @endcode + * + * The following classes are supported for the Pedals shield, v1: + * * SimRacing::LogitechPedals + * + * The following classes are supported for the Shifter shield, v1: + * * SimRacing::LogitechShifter (Driving Force) + * * SimRacing::LogitechShifterG923 (alias) + * * SimRacing::LogitechShifterG920 (alias) + * * SimRacing::LogitechShifterG29 (alias) + * + * Version 2 of the shifter shield includes support for all of the classes + * from v1, as well as the following: + * * SimRacing::LogitechShifterG27 + * * SimRacing::LogitechShifterG25 + * + * @note The default version of this template is undefined, so trying to + * create a class that is unsupported by the shield will generate + * a linker error. This is intentional. + * + * @tparam T The class to create + * @tparam Version The major version number of the shield + * + * @returns class instance, using the hardware pins on the shield + * + * @see https://github.com/dmadison/Sim-Racing-Shields + */ + template + T CreateShieldObject(); + + /** + * Create a LogitechPedals object for the Pedals Shield v1 + */ + template<> + LogitechPedals CreateShieldObject(); + + /** + * Create a LogitechPedals object for the Pedals Shield v2 + */ + template<> + LogitechPedals CreateShieldObject(); + + /** + * Create a LogitechShifter object for the Shifter Shield v1 + */ + template<> + LogitechShifter CreateShieldObject(); + + /** + * Create a LogitechShifter object for the Shifter Shield v2 + */ + template<> + LogitechShifter CreateShieldObject(); + + /** + * Create a LogitechShifterG27 object for the Shifter Shield v2 + */ + template<> + LogitechShifterG27 CreateShieldObject(); + + /** + * Create a LogitechShifterG25 object for the Shifter Shield v2 */ - #define PEDAL_SHIELD_V1_PINS A2, A1, A0, 10 + template<> + LogitechShifterG25 CreateShieldObject(); #endif } // end SimRacing namespace