User_m94dtr8j I'm using esp32 dev board with arduino with canbus transceiver SN65HVD230 (they work with 3.3V logic) and esp32 TWAIN canbus library.
Here's code. Code doesn't have rear door opening code (my clio iii is 3 door). The rear doors are on the Same CanID 0x60D, byte 1 (A) bits 6 (left door) and 7 (right door). The sketch is designed not to bombard the HU once all the doors are closed, sends the closed door command a few times and shuts up.
Steering wheel angle probably should be mapped to - 520, 520 but I'll have confirmation once I calibrate 360 cameras so I can see exactly how the lines move and if they are consistent with steering wheel full lock.
I have decoded much more info, like indicators, lights etc, but the commands to HU for these are unknown. Work in progress.
Canbus version in HU is volkswagen PQ Low.
If I add any other options, I'll post them here.
#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_BAUD_RATE = 500000;
const int UART_TX_PIN = 17;
const long UART_BAUD_RATE = 38400;
HardwareSerial UartPort(1);
const uint8_t CMD_STATUS = 0x41;
const uint8_t CMD_STEERING_WHEEL = 0x26;
const uint8_t SUB_CMD_DOOR_INFO = 0x01;
const uint8_t SUB_CMD_VEHICLE_INFO = 0x02;
const long VEHICLE_INFO_SEND_INTERVAL_MS = 500;
const long STEERING_SEND_INTERVAL_MS = 200;
const long DOOR_REPEAT_INTERVAL_MS = 333;
const long DOOR_CLOSED_GRACE_PERIOD_MS = 2000;
// =========================================================================
// Global Variables
// =========================================================================
CanFrame rxFrame;
#define STATE_DOORS_QUIET 0
#define STATE_DOORS_OPEN 1
#define STATE_DOORS_JUST_CLOSED 2
int doorSendingState = STATE_DOORS_QUIET;
int outsideTemperature = 15;
uint32_t odometerValue = 0;
uint16_t engineRpm = 0;
float steeringAngleTrue = 0.0;
int16_t steeringAngleMapped = 0;
uint8_t currentDoorStateMask = 0;
uint8_t previousDoorStateMask = 0;
unsigned long lastVehicleInfoSendTime = 0;
unsigned long lastSteeringSendTime = 0;
unsigned long lastDoorSendTime = 0;
unsigned long doorsClosedTimestamp = 0;
// =========================================================================
// Helper Functions
// =========================================================================
long map_float(float x, float in_min, float in_max, float out_min, float out_max) {
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}
void sendCanboxMessage(uint8_t cmd, uint8_t* data, uint8_t len) {
uint8_t sum = cmd + len;
for (int i = 0; i < len; i++) { sum += data[i]; }
uint8_t checksum = sum ^ 0xFF;
uint8_t finalFrame[len + 4];
finalFrame[0] = 0x2E;
finalFrame[1] = cmd;
finalFrame[2] = len;
memcpy(&finalFrame[3], data, len);
finalFrame[3 + len] = checksum;
UartPort.write(finalFrame, sizeof(finalFrame));
}
void sendDoorStatusCommand(uint8_t doorStateMask) {
uint8_t dataPayload[] = {SUB_CMD_DOOR_INFO, doorStateMask};
sendCanboxMessage(CMD_STATUS, dataPayload, sizeof(dataPayload));
Serial.print(" Sent Door Status. Mask: ");
Serial.println(doorStateMask, BIN);
}
void sendVehicleInfoCommand() {
uint16_t temp_encoded = outsideTemperature * 10;
uint8_t dataPayload[13] = {
SUB_CMD_VEHICLE_INFO,
(uint8_t)(engineRpm >> 8), (uint8_t)(engineRpm),
0, 0, 0, 0,
(uint8_t)(temp_encoded >> 8), (uint8_t)(temp_encoded),
(uint8_t)(odometerValue >> 16), (uint8_t)(odometerValue >> 8), (uint8_t)(odometerValue),
0
};
sendCanboxMessage(CMD_STATUS, dataPayload, sizeof(dataPayload));
Serial.print(" Sent Vehicle Info.");
Serial.printf(" RPM:%d, Temp:%d, Odo:%d\n", engineRpm, outsideTemperature, odometerValue);
}
void sendSteeringAngleCommand() {
uint8_t dataPayload[2] = {(uint8_t)(steeringAngleMapped), (uint8_t)(steeringAngleMapped >> 8)};
sendCanboxMessage(CMD_STEERING_WHEEL, dataPayload, sizeof(dataPayload));
Serial.print(" Sent Steering Angle. True: ");
Serial.print(steeringAngleTrue, 1);
Serial.print(", Mapped: ");
Serial.println(steeringAngleMapped);
}
// =========================================================================
// SETUP
// =========================================================================
void setup() {
Serial.begin(115200);
Serial.println("\nStarting Combined CAN Sender (Corrected Steering Range)...");
UartPort.begin(UART_BAUD_RATE, SERIAL_8N1, -1, UART_TX_PIN);
ESP32Can.setPins(CAN_TX, CAN_RX);
ESP32Can.setSpeed(ESP32Can.convertSpeed(CAN_BAUD_RATE));
if (ESP32Can.begin()) {
Serial.println("CAN bus started!");
} else {
Serial.println("Error starting CAN bus!");
}
}
// =========================================================================
// LOOP
// =========================================================================
void loop() {
if (ESP32Can.readFrame(rxFrame)) {
switch (rxFrame.identifier) {
case CAN_ID_STATUS_DOORS_TEMP:
if (rxFrame.data_length_code >= 5) {
int newTemp = round((float)rxFrame.data[4] - 40.0);
if (newTemp != outsideTemperature) {
outsideTemperature = newTemp;
Serial.printf("CAN [0x60D]: New temp: %dC\n", outsideTemperature);
}
uint8_t byteA = rxFrame.data[0];
uint8_t newDoorStateMask = 0;
if (byteA & (1 << 4)) newDoorStateMask |= 0b00000001;
if (byteA & (1 << 3)) newDoorStateMask |= 0b00000010;
currentDoorStateMask = newDoorStateMask;
}
break;
case CAN_ID_ODOMETER:
if (rxFrame.data_length_code >= 3) {
uint32_t rawOdo = ((uint32_t)rxFrame.data[0] << 16) | ((uint32_t)rxFrame.data[1] << 8) | rxFrame.data[2];
uint32_t newOdo = rawOdo / 16;
if (newOdo != odometerValue) {
odometerValue = newOdo;
Serial.printf("CAN [0x5FD]: New odometer: %d km\n", odometerValue);
}
}
break;
case CAN_ID_RPM:
if (rxFrame.data_length_code >= 2) {
uint16_t rawRpm = ((uint16_t)rxFrame.data[0] << 8) | rxFrame.data[1];
uint16_t newRpm = rawRpm / 8;
if (newRpm != engineRpm) {
engineRpm = newRpm;
Serial.printf("CAN [0x181]: New RPM: %d\n", engineRpm);
}
}
break;
case CAN_ID_STEERING:
if (rxFrame.data_length_code >= 2) {
uint16_t rawValue = (uint16_t)((rxFrame.data[1] << 8) | rxFrame.data[0]);
if (rawValue == 0xFFFF) {
Serial.println("CAN [0x0C2]: Steering sensor error received (0xFFFF)");
break;
}
float newTrueAngle = (float)(rawValue - 32768) / 10.0;
// --- THIS IS THE CORRECTED MAPPING ---
// Map the true angle from its known operational range to the head unit's range
int16_t newMappedAngle = map_float(newTrueAngle, -780.0, 780.0, -540.0, 540.0);
if (newMappedAngle != steeringAngleMapped) {
steeringAngleTrue = newTrueAngle;
steeringAngleMapped = newMappedAngle;
Serial.printf("CAN [0x0C2]: Raw: %u, True Angle: %.1f, Mapped: %d\n", rawValue, steeringAngleTrue, steeringAngleMapped);
}
}
break;
}
}
// --- Handle State Changes and Timed Sends ---
unsigned long currentTime = millis();
if (currentDoorStateMask != previousDoorStateMask) {
if (currentDoorStateMask > 0) {
doorSendingState = STATE_DOORS_OPEN;
Serial.println("State Machine -> DOORS_OPEN");
} else {
doorSendingState = STATE_DOORS_JUST_CLOSED;
doorsClosedTimestamp = millis();
Serial.println("State Machine -> DOORS_JUST_CLOSED (2s grace period)");
}
sendDoorStatusCommand(currentDoorStateMask);
previousDoorStateMask = currentDoorStateMask;
lastDoorSendTime = currentTime;
}
if (currentTime - lastVehicleInfoSendTime >= VEHICLE_INFO_SEND_INTERVAL_MS) {
lastVehicleInfoSendTime = currentTime;
sendVehicleInfoCommand();
}
if (currentTime - lastSteeringSendTime >= STEERING_SEND_INTERVAL_MS) {
lastSteeringSendTime = currentTime;
sendSteeringAngleCommand();
}
if (currentTime - lastDoorSendTime >= DOOR_REPEAT_INTERVAL_MS) {
lastDoorSendTime = currentTime;
if (doorSendingState == STATE_DOORS_OPEN) {
sendDoorStatusCommand(currentDoorStateMask);
} else if (doorSendingState == STATE_DOORS_JUST_CLOSED) {
sendDoorStatusCommand(0);
if (currentTime - doorsClosedTimestamp >= DOOR_CLOSED_GRACE_PERIOD_MS) {
doorSendingState = STATE_DOORS_QUIET;
Serial.println("State Machine -> DOORS_QUIET");
}
}
}
}