Building EcoShift: a Real-Time Eco-Driving Feedback System
by s215247237 in Circuits > Arduino
43 Views, 0 Favorites, 0 Comments
Building EcoShift: a Real-Time Eco-Driving Feedback System
EcoShift is a real-time eco-driving assistance system designed to help manual transmission drivers improve fuel efficiency. Using engine data collected from the vehicle's ECU and a custom gear detection system, EcoShift monitors engine RPM and provides visual and audible feedback when the driver remains in a gear longer than recommended.
Throughout a drive, the system records RPM violations, calculates an eco-score, and provides a post-drive summary to help drivers identify opportunities to improve their driving habits and reduce fuel consumption.
In this guide, you will build the EcoShift system from start to finish. This includes assembling the hardware, installing the required software, configuring the system, calibrating the sensors, and testing the completed implementation to ensure it operates correctly.
Supplies
For this project, you will need the following hardware and software:
Hardware:
- Arduino Nano 33 IoT
- Raspberry Pi
- OBD-II UART interface
- DB9-to-OBD-II cable
- Analog pressure sensor
- I2C LCD display
- Piezo buzzer
- x3 coloured LEDs
- Breadboard
- Jumper wires
- Micro USB cable
- Power bank
Software:
- Arduino IDE
- Node-RED
- Telegram app
- Public MQTT broker (e.g. EMQX)
Connecting to the ECU
Most of EcoShift's functionality relies on data obtained from the vehicle's ECU. This data is used to determine vehicle speed, engine RPM, and throttle position, calculate the current gear, identify active driving periods, detect RPM violations, generate alerts, and calculate the final eco-score.
To connect to the ECU, connect the TX, RX, and GND pins of the UART interface to the Arduino, ensuring the UART TX pin is connected to the Arduino RX pin and vice versa. Next, connect the DB9 cable between the UART interface and the vehicle's OBD-II port.
Most modern vehicles communicate internally using the Controller Area Network (CAN) bus. The OBD-II UART interface translates CAN messages into UART serial data that can be read and processed by the Arduino.
Before ECU data can be read, the OBD-II UART interface must be initialised. The functions below simplify ECU communication by handling command transmission and configuring the adapter for operation.
Place your equivalent of initialiseEcu() inside the setup() function in your Arduino sketch. This function will also be used later to recover from ECU communication failures.
A delay is included after each command to give the adapter time to process the request before the next command is sent. The values shown above were determined through testing and may need to be adjusted depending on the vehicle and OBD-II adapter being used. If Serial Monitor repeatedly displays messages such as:
Consider increasing the delay between commands.
The AT commands (ATZ, ATE0, etc.) reset the adapter, remove unnecessary formatting from responses, and enable automatic protocol detection. The final PID command (010D) requests vehicle speed and acts as a warm-up query to establish communication before normal operation begins.
Before continuing, verify that communication between the Arduino and ECU has been established correctly. Add the following code to your setup() function:
If a response similar to the one below is displayed in Serial Monitor:
Then communication between the Arduino and ECU has been established successfully. The exact values returned will vary between vehicles.
Cleaning the ECU Response
ECU responses often contain spaces, line breaks, and other formatting characters that are unnecessary for processing. Before extracting RPM, speed, and throttle values, these responses should be cleaned.
Create a helper function to remove this formatting:
For example, the raw response:
Becomes:
Once responses are being cleaned correctly, create a helper function that sends a PID request, reads the ECU response, and returns the cleaned result ready for processing.
Obtaining RPM, Speed, and Throttle Position
The next step is retrieving the ECU data used throughout the program. This consists of three values:
- 010C (Engine RPM)
- 010D (Vehicle speed)
- 0111 (Throttle position)
A helper function sends PID requests and returns a cleaned ECU response ready for processing.
The example below shows how engine RPM is obtained:
Create equivalent functions for vehicle speed (010D) and throttle position (0111) using the same approach.
To simplify the rest of the program, the latest RPM, speed, and throttle values are updated once per loop iteration and stored in global variables.
Before continuing, test these functions by printing their returned values to Serial Monitor to confirm that RPM, speed, and throttle position data are being read correctly from the ECU.
The resting value will vary between vehicles. In my vehicle, it was approximately 13%, so you may need to determine a suitable idle threshold for your own vehicle.
Updating ECU Data
Rather than repeatedly requesting RPM, speed, and throttle position throughout the program, create a helper function that retrieves the latest ECU values and stores them in global variables.
This function should be called once during each loop iteration to keep the stored values up to date. Storing the latest ECU readings in global variables improves efficiency by avoiding multiple ECU requests for the same data within a single loop iteration. Other functions can then access the most recent values directly rather than repeatedly calling the ECU reading functions.
Handling ECU Communication Failures
Because EcoShift relies heavily on ECU data, communication failures must be handled automatically.
One approach is to assume an ECU communication failure whenever RPM, speed, and throttle position all return invalid values:
When an ECU dropout is detected, initialiseEcu() (see Step 1) is called to re-establish communication with the OBD-II adapter. While communication is unavailable, features that rely on ECU data, including gear detection and RPM violation tracking, should be temporarily disabled.
Once valid ECU data is being returned again, normal operation can resume automatically.
Set a separate flag whenever an ECU dropout occurs during a drive. This flag can be used in the drive summary to warn the driver that the eco-score's accuracy may have been affected.
Do not clear the flag when communication is restored. Its purpose is to record that an ECU communication failure occurred at some point during the drive, even if the system later recovered successfully.
Setting Up the LCD Display
The LCD is used to provide the driver with important information throughout the drive. Typical information includes the current gear, RPM violation warnings, ECU status messages, gear resynchronisation confirmations, and the post-drive eco-score summary. It is one of three feedback components, which ensures that if a driver misses an alert or a hardware component fails, important information can still be communicated through the remaining feedback systems.
Limit LCD messages to relevant events and system states to minimise driver distraction. Mount the display somewhere the driver can quickly glance at it without taking their eyes off the road for extended periods, such as on the dashboard or instrument panel.
A standard I²C LCD display can be connected to the Arduino using VCC, GND, SDA, and SCL. In Arduino IDE, include the Wire library and an LCD library of your choice.
To avoid pausing the program, temporary messages should be managed using millis() rather than delay(). This allows warnings and notifications to be displayed for a fixed duration before automatically returning to the normal gear display.
A useful approach is to reserve the top line of the display for the current gear and use the second line for temporary notifications. For major events, such as ECU communication failures or the post-drive summary, both lines can be used to maximise available space.
Setting Up the Piezo Buzzer
The piezo buzzer provides audible alerts for events that require the driver's attention, such as RPM violations and ECU communication failures. Audible alerts are useful when the driver may not be looking directly at the LCD or LEDs.
To connect the buzzer, wire one pin to a digital pin on the Arduino and the other to GND. Mount the buzzer somewhere inside the cabin where it can be heard clearly without being excessively loud or distracting.
Create a helper function to manage buzzer alerts. This allows different frequencies and durations to be used for different events throughout the program.
Different alert patterns can then be used for different events. For example, RPM violation alerts can use repeating tones with an increasing frequency to encourage the driver to upshift, while ECU communication failures can use a single low-frequency tone to notify the driver of the issue.
Setting Up the LEDs
LEDs provide a simple visual way to communicate important events to the driver without requiring them to read the LCD or interpret buzzer tones.
Use different coloured LEDs to represent different system states. For example:
- Green LED: successful gear change
- Yellow LED: active RPM violation
- Red LED: ECU communication failure
Mount the LEDs somewhere the driver can easily glance at them, such as on the dashboard or alongside the LCD display.
To connect the LEDs, connect each anode to a separate digital pin on the Arduino so they can be controlled independently. Connect each cathode through a 220 Ω resistor and then to GND. The resistor limits the current flowing through the LED and helps prevent damage to both the LED and the Arduino.
Clutch Detection
Most vehicles do not report their current gear through the ECU. To determine the current gear, clutch detection can be combined with RPM and speed data to identify when a gear change occurs and estimate the selected gear.
The first step is detecting clutch movement. Because clutch position is not available through the ECU, an external sensor is required. A limit switch is well suited to this task. Mount the switch beside the clutch pedal so that it is held closed when the clutch is released and opens as soon as the pedal begins to move. This allows clutch movement to be detected immediately, regardless of whether the clutch is completely engaged or only being pressed slightly.
Use an interrupt to monitor clutch state changes. This ensures clutch movement is detected immediately, improving gear detection accuracy and reducing the likelihood of missed gear changes.
Connect the NC (normally closed) terminal of the limit switch to an interrupt-capable Arduino pin and connect the C (common) terminal to GND. Because the switch is mounted near the clutch pedal, long wire runs may be required. For reliability, ensure all wire joins are secured properly using solder, splices, or another suitable connection method.
Once installed, create an ISR to update a flag whenever the clutch state changes.
Keep the interrupt service routine as simple as possible. Rather than performing calculations inside the interrupt, use it only to update the clutch state and handle the rest of the gear detection logic elsewhere in the program.
Gear Detection
Most vehicles do not report their current gear through the ECU. However, it can be estimated using the relationship between engine RPM and vehicle speed:
To determine the gear ratios for your vehicle, print the calculated ratio to Serial Monitor and perform a test drive while collecting readings in each gear. Because RPM and speed fluctuate slightly during normal driving, record a range of values rather than a single ratio for each gear.
These ranges can then be used to determine the current gear. An example is shown below:
Once gear ratios have been established, create a function to detect when a gear change begins and another to determine when it has been completed. Create a global boolean variable to track whether a gear change is currently in progress. This flag is used to indicate that the clutch has been pressed and a new gear has not yet been calculated. When the clutch is pressed, set this flag to true, and once the new gear has been calculated, set it back to false.
A gear change begins when the clutch is pressed and is considered complete once the clutch has been released. This is because RPM and speed readings can be unstable while the clutch is engaged, making gear calculations less reliable during a shift. Waiting until the clutch is released allows the new gear to be calculated using more stable readings.
The short delay after clutch release provides an additional settling period for RPM and speed readings before the gear is calculated. Without this delay, gear detection may be less reliable due to rapidly changing sensor values immediately after a shift.
This approach calculates the gear only when a gear change occurs rather than continuously throughout the program, reducing unnecessary processing and allowing the current gear to be accessed directly whenever it is needed.
Setting Up the Resync Button
Because many features rely on the current gear being accurate, include a manual resynchronisation button. If a clutch press is missed or another issue causes the displayed gear to become inaccurate, the button can be used to manually recalculate the current gear using the RPM-to-speed ratios.
This provides a simple form of fault tolerance, allowing the correct gear to be re-established if a gear change is missed or detected incorrectly.
Configure the button as an interrupt so resync requests are detected immediately and the gear can be recalculated as quickly as possible.
Create a global flag that is set whenever the resync button is pressed. This allows manual resynchronisation requests to follow the same gear detection process used during a normal gear change.
Record whenever the resync button is used during a drive. This information can be included in the final drive summary to indicate that the eco-score's accuracy may have been affected by an earlier gear detection issue.
To connect the button, wire one terminal to an interrupt-capable Arduino pin and connect the other terminal to GND. Mount the button somewhere easily accessible to the driver so it can be pressed quickly when gear resynchronisation is required.
Reverse Detection
If your vehicle requires the gear stick to be pressed down before reverse can be selected, such as in some Volkswagen models, this movement can be detected using an analog pressure sensor mounted on the gear knob.
Position the pressure sensor where downward force is applied when selecting reverse and connect it to an unused analog pin on the Arduino. Ensure the wiring has enough slack to move freely with the gear stick during gear changes.
Before continuing, print the pressure sensor readings to Serial Monitor and repeatedly select reverse to determine a suitable threshold value.
Create a global flag to store whether reverse has been selected. In the detectGearShift() function, check whether the pressure reading exceeds the reverse threshold, the clutch is pressed, and the vehicle speed is below a low-speed threshold.
The clutch condition is used because reverse can only be selected while the clutch is engaged. The speed threshold helps prevent accidental reverse detection while driving.
In the getCurrentGear() function, check for reverse before checking any forward gear ratios:
Because reverse is determined using the pressure sensor rather than RPM-to-speed ratios, this check must appear before the forward gear calculations.
Only clear the reverse flag when a forward gear has been successfully detected:
This ensures reverse remains the reported gear until the driver intentionally selects a forward gear.
Neutral Detection
Unlike reverse, neutral does not require any additional hardware and can be inferred within the getCurrentGear() function.
The first neutral check occurs before the gear ratio calculation:
This prevents the RPM value from being divided by zero and allows the system to identify when the vehicle is stationary.
The second neutral check occurs after all forward gear ratio ranges have been evaluated:
If no gear ratio matches and the throttle position remains below the idle threshold, the vehicle is assumed to be in neutral.
These checks help prevent invalid speed, RPM, or ratio readings from being incorrectly interpreted as a valid gear.
Managing a Driving Session
A drive's duration is determined using active driving time. Active driving time is used throughout the program when calculating violation durations and the final eco-score.
Create a helper function that determines whether the vehicle is actively being driven. A vehicle is only considered to be actively driving when:
- The accelerator pedal is being pressed
- The vehicle is travelling above 0 km/h
- A forward gear is selected
- The clutch is not being pressed
- Valid ECU data is available
Only recording active driving time under these conditions helps ensure that violation tracking and eco-score calculations reflect genuine driving behaviour. Time spent stationary, reversing, pressing the clutch, or experiencing ECU communication failures is excluded.
Tracking Active Driving Time
Track active driving time using millis() rather than delay(). Create a helper function that calculates the time elapsed since the previous loop iteration and stores the result in timeSinceLastUpdate.
Next, create an updateActiveDrivingTime() function. Whenever isActivelyDriving() returns true, add timeSinceLastUpdate to a global activeDrivingTime variable.
Tracking active driving time by gear is also useful. Store the accumulated time for each forward gear in an array so it can later be used for violation tracking and post-drive feedback.
Determining When a Drive Is Complete
A drive should only be considered complete when the vehicle is stationary and the resync button (see Step 11) has been held for more than one second.
To determine whether the vehicle is stationary, verify that:
- Vehicle speed is 0 km/h
- The clutch is released
- The accelerator pedal is not being pressed
- The vehicle is in neutral
Requiring both the stationary conditions and a long button press helps prevent accidental drive completion while stopped at traffic lights, intersections, or in traffic.
Once these conditions have been met, the drive can be marked as complete:
Setting RPM Thresholds
RPM violations are detected by comparing the current engine RPM against a maximum RPM threshold for the selected gear.
Determine suitable thresholds for your vehicle through testing. The values below were used during development:
Create a helper function that returns the appropriate RPM threshold for the current gear:
Return 0 for reverse, neutral, the highest gear, and any invalid gear. Reverse, neutral, and invalid gears are excluded because an RPM violation is not applicable. The highest gear is excluded because shifting to a higher gear is not possible.
Detecting RPM Violations
Exceeding the recommended RPM threshold does not immediately result in a violation. During normal driving, RPM may briefly exceed the threshold while accelerating, overtaking, or completing a gear change. To prevent these short spikes from being unfairly penalised, the threshold must be exceeded continuously for a defined period before a violation is recorded.
Use separate timing thresholds for upshift and downshift recommendations:
A longer threshold should be used for downshifts because higher RPM is often expected immediately after changing to a lower gear and therefore should be given additional tolerance.
Create logic to track how long the RPM remains above the threshold. When the threshold is first exceeded, start a timer. If the RPM remains above the threshold for longer than the active duration, set the violation flag to true. If the RPM falls below the threshold before the timer expires, reset the timer and do not record a violation.
If the current gear is not being monitored, the violation state is reset and the function exits. Otherwise, a timer begins when the RPM threshold is exceeded and only becomes an active violation once the configured duration has elapsed.
Recording Violation Data
To track how long RPM violations occur in each gear, create an array that stores the accumulated violation time for every monitored gear:
Again, the highest gear is excluded because an upshift is not possible.
Next, create a helper function that adds timeSinceLastUpdate to the corresponding gear whenever an RPM violation is active:
This allows violation time to be accumulated throughout the drive while also identifying which gears contributed most to the final eco-score.
Alerting Driver of Violations
Recording RPM violations is useful for eco-score calculations, but providing feedback while a violation is occurring allows the driver to correct their behaviour immediately.
Create a helper function that checks whether an RPM violation is active. When a violation is detected, display a warning on the LCD, flash the warning LED, and sound the piezo buzzer.
To clearly communicate both the problem and the required action, alternate the LCD between messages such as:
As discussed in the Piezo Buzzer section (see Step 7), gradually increasing the buzzer frequency the longer the violation remains active can make the warning more noticeable. Beginning with a lower-frequency tone and increasing the pitch over time encourages an upshift without immediately producing an excessively intrusive alert.
Once the RPM violation ends, stop the buzzer, stop flashing the warning LED, clear the LCD warning message, and reset any alert counters so the next violation begins from its initial warning state.
Resetting Violations on Gear Change
Whenever a gear change occurs, reset the RPM violation state to prevent a violation that began in one gear from carrying over into the next.
Start by creating a variable to store the previously detected gear:
Next, create a helper function that compares the current gear to the previous gear. If the gear has changed, update the active violation duration, reset the violation state, and store the new gear.
Updating the active duration ensures the correct threshold is used for future violations. Resetting the timer and violation flag ensures each gear is evaluated independently.
Without this reset, a driver could exceed the RPM threshold in one gear, change gears, and immediately trigger a violation in the next gear without exceeding that gear's threshold for the required duration.
Calculating Eco-Score
Once active driving time and RPM violation time are being tracked, the eco-score can be calculated.
Begin by creating a helper function that sums all values stored in the rpmViolationTimeByGear array (see Step 19) to determine the total RPM violation time for the drive. These per-gear values can also be displayed in the post-drive summary to help drivers identify which gears contributed most to their eco-score.
Next, calculate the percentage of active driving time spent in an RPM violation state:
This represents the percentage of active driving time spent exceeding the recommended RPM thresholds.
The eco-score can then be calculated by subtracting the violation percentage from 100:
Ensure the eco-score cannot fall below 0.
Displaying the Drive Summary
Once the eco-score has been calculated, display a post-drive summary on the LCD. This summary can include:
- Final eco-score (%)
- RPM violation time by gear
- Driving advice based on recorded violations
- Accuracy warnings for gear resynchronisation or ECU communication failures
To generate driving advice, create a helper function that identifies the gear with the highest accumulated RPM violation time. This information can then be used to generate targeted feedback, such as:
Where X is the gear with the highest recorded RPM violation time.
If no violations were recorded, display a positive confirmation message instead, such as:
If a gear resynchronisation or ECU dropout occurred during the drive, display a warning that the eco-score's accuracy may have been affected.
Because the drive has already ended and no further monitoring is required, using delay() to control the display duration of summary screens is acceptable.
Setting Up Drive Summary Notifications on a Phone
While the LCD provides immediate feedback at the end of a drive, it can only display a limited amount of information and does not provide a permanent record of the results. Sending the drive summary to a phone allows the results to be reviewed later and retained for future reference.
To receive drive summaries on a phone, install the Telegram app and create a Telegram bot using BotFather. The bot will later be used by Node-RED to deliver completed drive summaries directly to the driver's phone.
Begin by opening Telegram and searching for @BotFather. Start a conversation and send the commands:
and
Follow the prompts to choose a name and username for your bot. Once complete, BotFather will provide a bot token. Save this token, as it will be required in the following step.
Before continuing, open a conversation with your newly created bot and press Start. This registers your account with the bot and allows messages to be delivered.
Connecting the Arduino to Wi-Fi and MQTT
Before drive summaries can be transmitted, the Arduino Nano 33 IoT must be configured to connect to both a Wi-Fi network and an MQTT broker.
Begin by including the WiFiNINA and PubSubClient libraries in your sketch.
Create variables to store the Wi-Fi network name, password, MQTT broker address, and MQTT topic.
This project uses the public EMQX broker, which does not require account creation, authentication, or additional configuration.
During development, a mobile phone hotspot was used rather than a home Wi-Fi network. This allows drive summaries to be transmitted even when the vehicle is away from home.
The MQTT topic acts as a communication channel between EcoShift and Node-RED and should be named something descriptive, such as:
Next, create helper functions to establish and maintain both the Wi-Fi and MQTT connections. These functions should be called periodically throughout the program to automatically reconnect if either connection is lost during a drive.
Setting Up the Message Forwarding Service
The Raspberry Pi hosts a Node-RED flow that acts as an intermediary between EcoShift and Telegram. While the Arduino is responsible for collecting and transmitting drive data, Node-RED receives this information via MQTT and forwards it to the driver's phone.
Begin by installing Node-RED on the Raspberry Pi. Once installed, access the Node-RED editor from a web browser on any device connected to the same local network by navigating to the Raspberry Pi's IP address on port 1880. The Raspberry Pi's IP address can be obtained by running:
Within the Node-RED editor, create a flow containing an MQTT input node, a debug node, and a Telegram sender node. Connect the MQTT input node to both the debug node and the Telegram sender node. The debug node can be used during testing to verify that messages are being received correctly before they are forwarded to Telegram.
Open the MQTT input node and enter the broker address and MQTT topic defined in the previous step. Once complete, deploy the flow and publish a test message from the Arduino. If the message appears in the debug panel, communication between the Arduino and Node-RED has been established successfully.
Finally, open the Telegram sender node and enter the bot token obtained from BotFather in Step 24. Select the chat that will receive drive summaries and deploy the updated flow. Once deployed, publish another test message from the Arduino and verify that it is delivered successfully to Telegram.
Sending the Drive Summary to a Phone
Once the drive summary has been generated, it can be transmitted to the MQTT topic defined in Step 25.
Begin by creating a helper function that formats the collected drive data into a text-based summary. A simplified example is shown below:
Once the summary has been generated, publish it to the MQTT topic using the MQTT client:
When a drive is completed, Node-RED will receive the published message via MQTT and automatically forward it to the driver's phone through Telegram. An example of the completed drive summary is shown in the image at the beginning of this step.
Creating the Main Program Loop
At this stage, all major EcoShift components have been implemented. The final step is combining these components within the loop() function so that they execute continuously while the system is running.
Begin by calling the helper functions responsible for maintaining Wi-Fi and MQTT connectivity. Next, update ECU data, gear detection, RPM violation tracking, timers, alerts, and LCD messages. Finally, check whether a drive has been completed and, if so, display and transmit the drive summary.
A simplified example is shown below:
The exact structure of your loop may differ depending on how your implementation is organised. Ensure that each subsystem is updated regularly and that long blocking delays are avoided where possible to maintain system responsiveness.
Congratulations! You now have a fully functional EcoShift system.
Happy driving!
Note: EcoShift is part of assignment submitted to Deakin University, School of IT, Unit SIT210/730 - Embedded Systems Development