mesyounes In this project you wont be able to use Simplesoft adaptor as the protocol isn't something I know, so cannot translate.
What works:
RPM, Speed, Steering Wheel Angle, Outside Temperature, All Lights & Indicators, All Doors, Odometer, All Steering Wheel Controls.
What DOESN'T work:
Average consumption (not present in Clio III), Seatbet & Parking Brake (yet, present in Clio III, protocol known, errors), TPMS (my Clio III does not have it) etc,
For all engine/body protocol for Toyota RAV4 2018-2020 refer here: https://drive.google.com/file/d/1iFQtRCPRPOZsFAjxb1eWVp3IcNeYncvQ/view
CAN Bus Button Matrix Clio III Carminat TomTom (Multimedia Can) - warning: screen and BIC must be left alone, tuck them under the dash somewhere. The SWC buttons are a matrix switches > then go to BIC module under dash and from the BIC module Multimedia CAN is taken to OBDII port on pins 12/13. I have no idea if in the normal Clio III with amber small screen the CAN is the same.
All frames are sent on CAN ID 0x58F with a Data Length of 8. Canbus speed 500kbps (same as main one)
Button Name Action CAN Bus Data Payload
- Volume Up Short Press 89 00 03 BB BB BB BB BB
- Long Press 89 00 43 BB BB BB BB BB
- Volume Down Short Press 89 00 04 BB BB BB BB BB
- Long Press 89 00 44 BB BB BB BB BB
- Next Track Short Press 89 01 01 BB BB BB BB BB
- Long Scroll 89 01 02 BB BB BB BB BB
- Previous Track Short Press 89 01 41 BB BB BB BB BB
- Long Scroll 89 01 42 BB BB BB BB BB
- Mute Short Press 89 00 05 BB BB BB BB BB
- Source/Audio Short Press 89 00 02 BB BB BB BB BB
- Telephone Short Press 89 00 01 BB BB BB BB BB
- Voice Short Press 89 00 06 BB BB BB BB BB
- OK Short Press 89 00 00 BB BB BB BB BB
- (Any Button) Release 80 01 BB BB BB BB BB
There are two ESP32's in the project, each with a SN65HVD230 CJMCU-230 CANBUS transceivers (one for SWC canbus and the second one for the engine/body canbus). SWC translates the SWC commands into Toyota RAV4 protocol and injects them into main ESP32. SWC commands have priority over engine and body messages, as any delay with volume, track etc would be annoying. The pin numbering in the Arduino sketches is according to the schematic here (dirty schematic, I can't be bothered to make it "pretty"): https://drive.google.com/file/d/1jZfsXzPao81jlkQmT2NBGCT_h3H_-lT7/view
SWC ESP32 maps the buttons to closest equivalent in the Toyota RAV4 protocol. SOURCE button doesn't do anything so feel free to map it to anything you like as per protocol here: https://drive.google.com/file/d/1iFL_Nui0X3n9P1Bsn-8NDJzWbIL6fmh6/view
Now, MUTE is long press of SOURCE/MODE, however, none of the long presses work in 3.6 beta DUDU from 05 September (let's wait, it was working before that update).
Engine/body: there is known Canbus address for seatbelt and parking brake in Clio III, however it is not yet in the sketch for ESP32 as there was a DUDU error and it was throwing immediately automatic gearbox icons etc. I am not sure if they fixed it in this update, I will check in the following week. If so, I will update the sketch.
ESP Engine/Body
#include <ESP32-TWAI-CAN.hpp>
#include <HardwareSerial.h>
#include <cmath>
// =========================================================================
// Configuration
// =========================================================================
#define CAN_TX 5
#define CAN_RX 4
const int CAN_ID_STATUS_DOORS_TEMP = 0x60D;
const int CAN_ID_ODOMETER = 0x5FD;
const int CAN_ID_RPM = 0x181;
const int CAN_ID_STEERING = 0x0C2;
const int CAN_ID_PARKING_BRAKE = 0x5C5;
const int CAN_ID_SPEED_BRAKE = 0x354;
const int CAN_BAUD_RATE = 500000;
// --- Steering Angle Calibration ---
const int STEERING_RAW_LEFT = 148;
const int STEERING_RAW_RIGHT = 107;
const int STEERING_FINAL_LEFT = -540;
const int STEERING_FINAL_RIGHT = 540;
// --- UART Setup for Headunit (Output) ---
const int UART_TX_PIN = 17;
const long UART_BAUD_RATE = 38400;
HardwareSerial UartPort(1);
// --- UART Setup for Injector (Input from SWC ESP32) ---
const int UART_INJECTOR_RX_PIN = 25; // GPIO pin to receive data from the other ESP32
const int UART_INJECTOR_TX_PIN = 26; // Not used, but required for initialization
const long UART_INJECTOR_BAUD_RATE = 38400; // Must match the sending ESP32's baud rate
HardwareSerial UartInjectorPort(2); // Using UART2
// --- CAN->UART Command Bytes ---
const uint8_t CMD_DOOR_STATUS = 0x24;
const uint8_t CMD_MULTI_FUNCTION = 0x7D;
const uint8_t CMD_STEERING_WHEEL = 0x29;
const uint8_t CMD_OUTSIDE_TEMP = 0x28;
// --- Send Intervals (ms) ---
const long STEERING_SEND_INTERVAL_MS = 200;
const long STATUS_SEND_INTERVAL_MS = 250;
const long DOOR_PERIODIC_INTERVAL_MS = 2000;
const long SPEED_SEND_INTERVAL_MS = 500;
const long RPM_SEND_INTERVAL_MS = 333;
const long TEMP_SEND_INTERVAL_MS = 5000;
const long ODOMETER_SEND_INTERVAL_MS = 10000;
const long INDICATOR_TIMEOUT_MS = 500;
// --- UART Bitmasks for Doors ---
const uint8_t MASK_DOOR_DRIVER = 1 << 7;
const uint8_t MASK_DOOR_PASSENGER = 1 << 6;
const uint8_t MASK_DOOR_BOOT = 1 << 3;
// Global Variables
CanFrame rxFrame;
int outsideTemperature = 15;
uint32_t odometerValue = 0;
uint16_t engineRpm = 0;
int steeringAngleRaw = 0;
int16_t rczSteeringAngle = 0;
uint8_t currentDoorStateMask = 0;
bool isParkingBrakeOn = false;
bool lastParkingBrakeState = false;
float vehicleSpeed = 0.0;
float tripDistance = 0.0;
bool isBrakePedalPressed = false;
bool headlightsOn = false;
bool highBeamsOn = false;
bool parkingLightsOn = false;
uint8_t lastLightsStateByte = 0;
unsigned long lastLeftIndicatorSignalTime = 0;
unsigned long lastRightIndicatorSignalTime = 0;
unsigned long lastSteeringSendTime = 0;
unsigned long lastStatusSendTime = 0;
unsigned long lastDoorPeriodicSendTime = 0;
unsigned long lastSpeedSendTime = 0;
unsigned long lastRpmSendTime = 0;
unsigned long lastTempSendTime = 0;
unsigned long lastOdometerSendTime = 0;
void sendCanboxMessage(uint8_t cmd, const uint8_t* data, uint8_t len) {
uint8_t sum = cmd + len;
uint8_t frame[len + 4];
frame[0] = 0x2E;
frame[1] = cmd;
frame[2] = len;
for (uint8_t i = 0; i < len; ++i) {
sum += data[i];
frame[3 + i] = data[i];
}
frame[3 + len] = sum ^ 0xFF;
UartPort.write(frame, len + 4);
}
void sendDoorCommand(uint8_t doorMask) { sendCanboxMessage(CMD_DOOR_STATUS, &doorMask, 1); }
void sendOutsideTempMessage(int temp) {
uint8_t encoded = uint8_t((temp + 40) * 2);
uint8_t payload[12] = {0};
payload[5] = encoded;
sendCanboxMessage(CMD_OUTSIDE_TEMP, payload, 12);
}
void sendLightsUartMessage(uint8_t lightsState) {
uint8_t payload[2] = { 0x01, lightsState };
sendCanboxMessage(CMD_MULTI_FUNCTION, payload, 2);
}
void sendOdometerUartMessage(uint32_t odo) {
uint8_t payload[12] = { 0x04, uint8_t(odo), uint8_t(odo >> 8), uint8_t(odo >> 16), 0,0,0,0,0,0,0,0 };
sendCanboxMessage(CMD_MULTI_FUNCTION, payload, 12);
}
void sendRpmUartMessage(uint16_t rpm) {
uint16_t half = rpm / 2;
uint8_t payload[3] = { 0x0A, uint8_t(half & 0xFF), uint8_t(half >> 8) };
sendCanboxMessage(CMD_MULTI_FUNCTION, payload, 3);
}
void sendSpeedUartMessage(float speed) {
uint16_t raw = uint16_t(speed * 100.0f);
uint8_t payload[5] = { 0x03, uint8_t(raw & 0xFF), uint8_t(raw >> 8), 0x00, 0x00 };
sendCanboxMessage(CMD_MULTI_FUNCTION, payload, 5);
}
void sendSteeringAngleMessage(int16_t angle) {
uint8_t payload[2] = { uint8_t(angle & 0xFF), uint8_t(angle >> 8) };
sendCanboxMessage(CMD_STEERING_WHEEL, payload, 2);
}
void setup() {
Serial.begin(115200);
// Initialize UART port to the headunit
UartPort.begin(UART_BAUD_RATE, SERIAL_8N1, -1, UART_TX_PIN);
// Initialize UART port for message injection
UartInjectorPort.begin(UART_INJECTOR_BAUD_RATE, SERIAL_8N1, UART_INJECTOR_RX_PIN, UART_INJECTOR_TX_PIN);
Serial.println("Injector UART port initialized. Listening for messages...");
ESP32Can.setPins(CAN_TX, CAN_RX);
ESP32Can.setSpeed(ESP32Can.convertSpeed(CAN_BAUD_RATE));
ESP32Can.begin();
}
void loop() {
unsigned long now = millis();
bool swcMessageHandled = false;
// --- Priority handling for SWC messages ---
if (UartInjectorPort.available()) {
swcMessageHandled = true; // Flag that we are handling a priority message
while (UartInjectorPort.available()) {
// Read one byte from the injector port
uint8_t byteFromOtherEsp = UartInjectorPort.read();
// Immediately write (forward) that byte to the headunit port
UartPort.write(byteFromOtherEsp);
}
}
// If a steering wheel control message was just forwarded,
// skip the rest of the loop to give it priority and prevent interleaving.
if (swcMessageHandled) {
return;
}
// --- Read CAN frames (only if no SWC message was handled) ---
if (ESP32Can.readFrame(rxFrame)) {
switch (rxFrame.identifier) {
case CAN_ID_STATUS_DOORS_TEMP:
if (rxFrame.data_length_code >= 5) {
outsideTemperature = round(float(rxFrame.data[4]) - 40.0f);
uint32_t status = (uint32_t(rxFrame.data[0]) << 16) | (uint32_t(rxFrame.data[1]) << 8) | uint32_t(rxFrame.data[2]);
if (status & (1UL << 13)) lastLeftIndicatorSignalTime = now;
if (status & (1UL << 14)) lastRightIndicatorSignalTime = now;
headlightsOn = status & (1UL << 17);
highBeamsOn = status & (1UL << 11);
parkingLightsOn = status & (1UL << 18);
uint8_t mask = 0;
if (status & (1UL << 20)) mask |= MASK_DOOR_DRIVER;
if (status & (1UL << 19)) mask |= MASK_DOOR_PASSENGER;
if (status & (1UL << 23)) mask |= MASK_DOOR_BOOT;
currentDoorStateMask = mask;
}
break;
case CAN_ID_ODOMETER:
if (rxFrame.data_length_code >= 3) {
uint32_t raw = (uint32_t(rxFrame.data[0]) << 16) | (uint32_t(rxFrame.data[1]) << 8) | uint32_t(rxFrame.data[2]);
odometerValue = raw / 16;
}
break;
case CAN_ID_RPM:
if (rxFrame.data_length_code >= 2) {
engineRpm = (uint16_t(rxFrame.data[0]) << 8) | uint16_t(rxFrame.data[1]);
}
break;
case CAN_ID_STEERING:
if (rxFrame.data_length_code >= 1) {
steeringAngleRaw = rxFrame.data[0];
rczSteeringAngle = map(steeringAngleRaw, STEERING_RAW_LEFT, STEERING_RAW_RIGHT, STEERING_FINAL_RIGHT, STEERING_FINAL_LEFT);
}
break;
case CAN_ID_PARKING_BRAKE:
if (rxFrame.data_length_code >= 1) {
isParkingBrakeOn = (rxFrame.data[0] & (1 << 2)) == 0;
}
break;
case CAN_ID_SPEED_BRAKE:
if (rxFrame.data_length_code >= 5) {
uint16_t rawSpeed = (uint16_t(rxFrame.data[0]) << 8) | uint16_t(rxFrame.data[1]);
vehicleSpeed = rawSpeed * 0.01f;
uint16_t rawDist = (uint16_t(rxFrame.data[3]) << 8) | uint16_t(rxFrame.data[2]);
tripDistance = rawDist * 0.1f;
isBrakePedalPressed = (rxFrame.data[4] & (1 << 4)) != 0;
}
break;
}
}
// --- Lights & indicators on-change ---
bool leftActive = (now - lastLeftIndicatorSignalTime < INDICATOR_TIMEOUT_MS);
bool rightActive = (now - lastRightIndicatorSignalTime < INDICATOR_TIMEOUT_MS);
uint8_t lightMask = 0;
if (rightActive) lightMask |= 0x08;
if (leftActive) lightMask |= 0x10;
if (highBeamsOn) lightMask |= 0x20;
if (headlightsOn) lightMask |= 0x40;
if (parkingLightsOn) lightMask |= 0x80;
if (lightMask != lastLightsStateByte) {
sendLightsUartMessage(lightMask);
lastLightsStateByte = lightMask;
}
// --- Scheduled sends ---
if (now - lastSteeringSendTime >= STEERING_SEND_INTERVAL_MS) {
lastSteeringSendTime = now;
sendSteeringAngleMessage(rczSteeringAngle);
}
if (now - lastStatusSendTime >= STATUS_SEND_INTERVAL_MS) {
lastStatusSendTime = now;
sendDoorCommand(currentDoorStateMask);
}
if (now - lastDoorPeriodicSendTime >= DOOR_PERIODIC_INTERVAL_MS) {
lastDoorPeriodicSendTime = now;
sendDoorCommand(currentDoorStateMask);
}
if (now - lastOdometerSendTime >= ODOMETER_SEND_INTERVAL_MS) {
lastOdometerSendTime = now;
sendOdometerUartMessage(odometerValue);
}
if (now - lastTempSendTime >= TEMP_SEND_INTERVAL_MS) {
lastTempSendTime = now;
sendOutsideTempMessage(outsideTemperature);
}
if (now - lastRpmSendTime >= RPM_SEND_INTERVAL_MS) {
lastRpmSendTime = now;
sendRpmUartMessage(engineRpm);
}
if (now - lastSpeedSendTime >= SPEED_SEND_INTERVAL_MS) {
lastSpeedSendTime = now;
sendSpeedUartMessage(vehicleSpeed);
}
}
ESP SWC
#include <ESP32-TWAI-CAN.hpp>
#include <HardwareSerial.h>
// --- Configuration ---
#define CAN_RX_PIN 5
#define CAN_TX_PIN 4
#define CAN_BAUD_RATE 500000
const uint32_t CAN_ID_STEERING_WHEEL = 0x58F;
// --- UART Output for Toyota Protocol ---
#define UART_TX_PIN 26 // Output pin for the translated commands
#define UART_RX_PIN 25 // Not used, but required for begin()
const long UART_BAUD_RATE = 38400;
// --- Keypress delay for standard buttons ---
#define KEYPRESS_DELAY_MS 120
// Use UART Port 1 for the output
HardwareSerial UartPort(1);
// State tracking variable
bool isButtonPressed = false;
/**
* @brief Constructs and sends a steering wheel control message via UART.
* @param keyCode The button identifier from the Toyota protocol.
* @param keyStatus The state of the button (0=released, 1=pressed, 2=long press).
*/
void sendToyotaUartMessage(uint8_t keyCode, uint8_t keyStatus) {
uint8_t frame[6];
frame[0] = 0x2e;
frame[1] = 0x20;
frame[2] = 0x02;
frame[3] = keyCode;
frame[4] = keyStatus;
uint8_t sum = frame[1] + frame[2] + frame[3] + frame[4];
frame[5] = sum ^ 0xFF;
Serial.printf("SWC TX -> KeyCode: 0x%02X, KeyStatus: %d\n", keyCode, keyStatus);
UartPort.write(frame, sizeof(frame));
// If the command was a press (status 1 or 2), add a short delay.
if (keyStatus > 0) {
delay(KEYPRESS_DELAY_MS);
}
}
/**
* @brief Translates a CAN bus frame into a Toyota UART command.
* @param frame The incoming CAN frame with steering wheel data.
*/
void translateCanToUart(const CanFrame& frame) {
if (frame.data_length_code != 8) {
return;
}
// Check for the RELEASE command (header 0x80)
if (frame.data[0] == 0x80) {
if (isButtonPressed) {
sendToyotaUartMessage(0x00, 0);
isButtonPressed = false;
}
return;
}
// Check for a PRESS command (header 0x89)
if (frame.data[0] == 0x89) {
uint8_t buttonGroup = frame.data[1];
uint8_t buttonCode = frame.data[2];
// --- Special Handling for MUTE button ---
if (buttonGroup == 0x00 && buttonCode == 0x05) {
// Send the MUTE command (SRC Long Press)
sendToyotaUartMessage(0x07, 2);
// Wait for one second
delay(1000);
// Automatically send the release command
sendToyotaUartMessage(0x00, 0);
// This button's entire cycle is handled, so exit.
return;
}
uint8_t uartKeyCode = 0x00;
uint8_t uartKeyStatus = 0;
bool isValidButton = true;
// Direct Mapping Logic for all other buttons
switch (buttonGroup) {
case 0x00: // Left-side CAN buttons
switch (buttonCode) {
case 0x03: uartKeyCode = 0x01; uartKeyStatus = 1; break; // VOL+ Short
case 0x43: uartKeyCode = 0x01; uartKeyStatus = 2; break; // VOL+ Long
case 0x04: uartKeyCode = 0x02; uartKeyStatus = 1; break; // VOL- Short
case 0x44: uartKeyCode = 0x02; uartKeyStatus = 2; break; // VOL- Long
case 0x02: uartKeyCode = 0x07; uartKeyStatus = 1; break; // SOURCE Short
// case 0x05: is now handled above
case 0x01: uartKeyCode = 0x88; uartKeyStatus = 1; break; // TEL
case 0x06: uartKeyCode = 0x08; uartKeyStatus = 1; break; // VOICE
case 0x00: uartKeyCode = 0x16; uartKeyStatus = 1; break; // OK
default: isValidButton = false; break;
}
break;
case 0x01: // Right-side CAN buttons
switch (buttonCode) {
case 0x01: uartKeyCode = 0x85; uartKeyStatus = 1; break; // NEXT+ Short
case 0x02: uartKeyCode = 0x85; uartKeyStatus = 2; break; // NEXT+ Long
case 0x41: uartKeyCode = 0x86; uartKeyStatus = 1; break; // PREV- Short
case 0x42: uartKeyCode = 0x86; uartKeyStatus = 2; break; // PREV- Long
default: isValidButton = false; break;
}
break;
default:
isValidButton = false;
break;
}
if (isValidButton) {
sendToyotaUartMessage(uartKeyCode, uartKeyStatus);
isButtonPressed = true;
}
}
}
void setup() {
Serial.begin(115200);
Serial.println("SWC CAN to UART Translator Initialized.");
UartPort.begin(UART_BAUD_RATE, SERIAL_8N1, UART_RX_PIN, UART_TX_PIN);
ESP32Can.setPins(CAN_TX_PIN, CAN_RX_PIN);
ESP32Can.setSpeed(ESP32Can.convertSpeed(CAN_BAUD_RATE));
if (!ESP32Can.begin()) {
Serial.println("Failed to start CAN bus. Halting.");
while (1);
}
Serial.println("CAN bus running. Listening for steering wheel controls...");
}
void loop() {
CanFrame rxFrame;
if (ESP32Can.readFrame(rxFrame)) {
if (rxFrame.identifier == CAN_ID_STEERING_WHEEL) {
translateCanToUart(rxFrame);
}
}
}
You can also use ESP32 Mini, saves space and really it could be all connected with the jumper wires and secured. If so, remember to change PIN assignments in the sketches.
The code does not have additional screen code (there is a header for 1.8 TFT screen on the schematic), neither there is a code for DPF soot and regeneration. It will be done in the second part of the project.
If you don't change anything in the SWC sketch, in DUDU go to Vehicle > SWC Learning > Mode Button and assign BT Phone as 1st app, rest Not assigned - TEL button on the wheel will bring BT Phone app full screen, second press will close it,.
There are a few other things to be done - front and rear radar. Protocol is now known to me. I am waiting for 12 Euros (x2) radars for front and rear. I have already cracked the radar communication and it can be translated to DUDU as well. So for 20 Euros you can have integrated front and rear radar into dudu.
Also, DUDU sends over canbus UART title and artist when changing songs, so it will also be incorporated into the protocol later.
Steering wheel Canbus Laguna III: 0x58F the frames: -0x89 0x00 0x03 0xA3 0xA3 0xA3 0xA3 0xA3 : vol + short press -0x89 0x00 0x43 0xA3 0xA3 0xA3 0xA3 0xA3 : vol + long press -0x89 0x00 0x04 0xA3 0xA3 0xA3 0xA3 0xA3 : vol- short press -0x89 0x00 0x44 0xA3 0xA3 0xA3 0xA3 0xA3 : vol- long press -0x89 0x00 0x05 0xA3 0xA3 0xA3 0xA3 0xA3 : mute -0x89 0x01 0x01 0xA3 0xA3 0xA3 0xA3 0xA3 : wheel down -0x89 0x01 0x41 0xA3 0xA3 0xA3 0xA3 0xA3 : wheel up -0x89 0x01 0x00 0xA3 0xA3 0xA3 0xA3 0xA3 : wheel press -0x89 0x00 0x07 0xA3 0xA3 0xA3 0xA3 0xA3 : left rear button -0x89 0x00 0x08 0xA3 0xA3 0xA3 0xA3 0xA3 : right rear button
I have also checked Renault VISIO and multimedia canbus seems to be present on 12/13 pins for every car trim, also for these with only amber display. Probably the can IDs are the same, either these one I gave or these ones from Laguna above (the other version of the steering stalk). This is also applicable to Megane III.