Got it working was using a adafruit canbus bff with an xiao esp32c6. But it just wouldn’t work. Changed to a different canbus module like others have used and works straight away. Building a web interface to control it. you just connect to the esp32 running in ap mode and can control everything.no initalisation needed
they start with 53.x V and full current.
orange/yellow light flashing >>> no can communication
You can´t damage anything with wrong connected can bus, it just will not comunicate


// This sketch configures the XIAO ESP32C6 to connect to a WiFi network
// (or create its own Access Point) and host a simple web server.
// The web page allows you to send commands to a Vertiv R48-2000e3
// power supply over CAN bus and display live measurement data.
// This version uses the mcp_can library, which you confirmed works for you.
// Include necessary libraries for ESP32, WiFi, WebServer, and mcp_can.
#include <SPI.h>
#include <mcp_can.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
// --- WiFi Configuration ---
// Set this to true to create an Access Point, false to connect to a network.
const bool WIFI_AP_MODE = false;
// If WIFI_AP_MODE is true, these will be the AP credentials.
const char* ap_ssid = "ESP32_Vertiv_AP";
const char* ap_password = "password123";
// If WIFI_AP_MODE is false, these will be your network credentials.
const char* ssid = "xxx";
const char* password = "xxx";
// --- Pinout Configuration for Seeed Studio XIAO ESP32C6 ---
// This uses the same pinout as your previous attempts, with CS on GPIO 21.
//
// MCP2515 Pin | XIAO ESP32C6 Pin | ESP32-C6 GPIO
// ------------|------------------|----------------
// MOSI | D10 | GPIO18
// MISO | D9 | GPIO20
// SCK | D8 | GPIO19
// CS | D3 | GPIO21 (Flexible, can be any available GPIO)
// INT | D2 | GPIO2 (Flexible, can be any available GPIO)
// Define the Chip Select pin for the MCP2515 CAN module
const int SPI_CS_PIN = 21;
// Create an instance of the MCP_CAN library with the Chip Select pin
MCP_CAN CAN0(SPI_CS_PIN);
// Create an AsyncWebServer instance on port 80
AsyncWebServer server(80);
// --- CAN Bus Definitions ---
// These are the CAN IDs based on the working example you provided.
const long VERTIV_COMMAND_ID = 0x06080783;
const long VERTIV_READ_REQUEST_ID = 0x06000783;
const long VERTIV_RESPONSE_ID = 0x860F8003; // Updated CAN ID based on your logs
const unsigned long CAN_BUS_SPEED = 125000; // 125 Kbps
// Enum for measurement numbers
enum MeasurementType {
OUTPUT_VOLTAGE = 0x01,
OUTPUT_CURRENT = 0x02,
OUTPUT_CURRENT_LIMIT = 0x03,
TEMPERATURE = 0x04,
SUPPLY_VOLTAGE = 0x05,
// Define command types for confirmation
SET_PERMANENT_VOLTAGE_CMD = 0x24,
SET_PERMANENT_CURRENT_LIMIT_CMD = 0x19
};
// --- Global variables to store the latest measurement data ---
// We'll initialize them with default values.
float outputVoltage = 0.0;
float outputCurrent = 0.0;
float outputCurrentLimit = 0.0;
float temperature = 0.0;
float supplyVoltage = 0.0;
// --- Variables for command delay logic ---
bool isCommandPending = false;
unsigned long commandSentTime = 0;
// Set the fixed delay for permanent commands to 45 seconds as requested
const unsigned long PERMANENT_COMMAND_DELAY = 45000; // 45 seconds
// --- HTML and JavaScript for the Web Page ---
// This entire string will be sent to the client when they access the root URL.
const char* html_page = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<title>Vertiv CAN Control</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { font-family: sans-serif; margin: 20px; background-color: #f0f0f0; }
.container { max-width: 600px; margin: auto; padding: 20px; background-color: #fff; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1); }
h1, h2 { color: #333; }
.data-card { background-color: #e9e9e9; padding: 15px; border-radius: 6px; margin-bottom: 10px; }
.data-card p { margin: 0; font-size: 1.2em; }
.data-card span { font-weight: bold; color: #007BFF; }
form { margin-top: 20px; padding: 15px; background-color: #f9f9f9; border-radius: 6px; }
input[type="number"], button { width: 100%; padding: 10px; margin-bottom: 10px; border-radius: 4px; border: 1px solid #ccc; box-sizing: border-box; }
button { background-color: #007BFF; color: white; border: none; cursor: pointer; font-size: 1em; }
button:hover:not(:disabled) { background-color: #0056b3; }
button:disabled { background-color: #ccc; cursor: not-allowed; }
.status-message {
background-color: #ffc107;
color: #333;
padding: 10px;
border-radius: 6px;
margin-top: 10px;
text-align: center;
font-weight: bold;
}
</style>
</head>
<body>
<div class="container">
<h1>Vertiv R48-2000e3 Control</h1>
<h2>Live Data</h2>
<div class="data-card">
<p>Output Voltage: <span id="outputVoltage">--</span> V</p>
</div>
<div class="data-card">
<p>Output Current: <span id="outputCurrent">--</span> A</p>
</div>
<div class="data-card">
<p>Current Limit: <span id="currentLimit">--</span> %</p>
</div>
<div class="data-card">
<p>Temperature: <span id="temperature">--</span> C</p>
</div>
<div class="data-card">
<p>Supply Voltage: <span id="supplyVoltage">--</span> V</p>
</div>
<div id="statusMessage" class="status-message" style="display: none;"></div>
<h2>Set Permanent Voltage</h2>
<form id="permVoltageForm">
<input type="number" step="0.1" name="value" placeholder="e.g., 52.5" required>
<button type="submit" class="command-button">Set Permanent Voltage</button>
</form>
<h2>Set Online Voltage</h2>
<form id="onlineVoltageForm">
<input type="number" step="0.1" name="value" placeholder="e.g., 50.0" required>
<button type="submit" class="command-button">Set Online Voltage</button>
</form>
<h2>Set Permanent Current Limit</h2>
<form id="permCurrentForm">
<input type="number" step="0.01" name="value" placeholder="e.g., 0.5 (for 50%)" required>
<button type="submit" class="command-button">Set Permanent Current Limit</button>
</form>
<h2>Set Online Current Limit</h2>
<form id="onlineCurrentForm">
<input type="number" step="0.01" name="value" placeholder="e.g., 0.5 (for 50%)" required>
<button type="submit" class="command-button">Set Online Current Limit</button>
</form>
<h2>Set Fan Speed</h2>
<form id="fanSpeedForm">
<button type="submit" name="speed" value="auto" class="command-button">Auto</button>
<button type="submit" name="speed" value="full" class="command-button">Full Speed</button>
</form>
<h2>Walk-in Control</h2>
<form id="walkInStateForm">
<button type="submit" name="state" value="on" class="command-button">Walk-in On</button>
<button type="submit" name="state" value="off" class="command-button">Walk-in Off</button>
</form>
<form id="walkInTimeForm">
<input type="number" step="1" name="value" placeholder="e.g., 10 (seconds)" required>
<button type="submit" class="command-button">Set Walk-in Time</button>
</form>
</div>
<script>
// Function to update the data display and button states
function updateData() {
fetch('/data')
.then(response => response.json())
.then(data => {
document.getElementById('outputVoltage').innerText = data.outputVoltage.toFixed(2);
document.getElementById('outputCurrent').innerText = data.outputCurrent.toFixed(2);
document.getElementById('currentLimit').innerText = (data.outputCurrentLimit * 100).toFixed(2);
document.getElementById('temperature').innerText = data.temperature.toFixed(2);
document.getElementById('supplyVoltage').innerText = data.supplyVoltage.toFixed(2);
const buttons = document.querySelectorAll('.command-button');
const messageBox = document.getElementById('statusMessage');
if (data.isCommandPending) {
buttons.forEach(button => button.disabled = true);
messageBox.style.display = 'block';
messageBox.innerText = 'Waiting for command to be processed... ' + data.remainingTime + ' seconds remaining';
} else {
buttons.forEach(button => button.disabled = false);
messageBox.style.display = 'none';
}
})
.catch(error => console.error('Error fetching data:', error));
}
// Set up form submission handlers
document.getElementById('permVoltageForm').addEventListener('submit', function(event) {
event.preventDefault();
const value = this.elements.value.value;
fetch('/set_perm_v', { method: 'POST', body: 'value=' + value, headers: { 'Content-Type': 'application/x-www-form-urlencoded' } })
.then(response => response.text())
.then(text => alert(text))
.catch(error => console.error('Error:', error));
});
document.getElementById('onlineVoltageForm').addEventListener('submit', function(event) {
event.preventDefault();
const value = this.elements.value.value;
fetch('/set_online_v', { method: 'POST', body: 'value=' + value, headers: { 'Content-Type': 'application/x-www-form-urlencoded' } })
.then(response => response.text())
.then(text => alert(text))
.catch(error => console.error('Error:', error));
});
document.getElementById('permCurrentForm').addEventListener('submit', function(event) {
event.preventDefault();
const value = this.elements.value.value;
fetch('/set_perm_c', { method: 'POST', body: 'value=' + value, headers: { 'Content-Type': 'application/x-www-form-urlencoded' } })
.then(response => response.text())
.then(text => alert(text))
.catch(error => console.error('Error:', error));
});
document.getElementById('onlineCurrentForm').addEventListener('submit', function(event) {
event.preventDefault();
const value = this.elements.value.value;
fetch('/set_online_c', { method: 'POST', body: 'value=' + value, headers: { 'Content-Type': 'application/x-www-form-urlencoded' } })
.then(response => response.text())
.then(text => alert(text))
.catch(error => console.error('Error:', error));
});
// New handler for fan speed form submission
document.getElementById('fanSpeedForm').addEventListener('submit', function(event) {
event.preventDefault();
const speed = event.submitter.value;
fetch('/set_fan_speed', { method: 'POST', body: 'speed=' + speed, headers: { 'Content-Type': 'application/x-www-form-urlencoded' } })
.then(response => response.text())
.then(text => alert(text))
.catch(error => console.error('Error:', error));
});
// New handler for walk-in state form submission (on/off)
document.getElementById('walkInStateForm').addEventListener('submit', function(event) {
event.preventDefault();
const state = event.submitter.value;
fetch('/set_walk_in', { method: 'POST', body: 'state=' + state, headers: { 'Content-Type': 'application/x-www-form-urlencoded' } })
.then(response => response.text())
.then(text => alert(text))
.catch(error => console.error('Error:', error));
});
// New form handler for walk-in time using a POST request
document.getElementById('walkInTimeForm').addEventListener('submit', function(event) {
event.preventDefault();
const value = this.elements.value.value;
fetch('/set_walk_in_time', { method: 'POST', body: 'value=' + value, headers: { 'Content-Type': 'application/x-www-form-urlencoded' } })
.then(response => response.text())
.then(text => alert(text))
.catch(error => console.error('Error:', error));
});
// Request data every 1 second to keep the countdown live
setInterval(updateData, 1000);
// Initial data fetch on page load
updateData();
</script>
</body>
</html>
)rawliteral";
// --- Function Prototypes ---
void setVertivVoltagePermanent(float voltage);
void setVertivVoltageOnline(float voltage);
void setVertivCurrentPermanent(float currentPercentage);
void setVertivCurrentOnline(float currentPercentage);
void readVertivSetting(byte measurementNo);
void processIncomingCanMessages();
void setVertivFanSpeed(bool fullSpeed);
void setVertivWalkIn(bool on);
void setVertivWalkInTime(float seconds);
// Helper function to convert 4 bytes (big-endian) to a float
float bytesToFloat(byte b[4]) {
union { float f; byte b[4]; } converter;
// Copy bytes with explicit order, assuming CAN data is big-endian
converter.b[3] = b[0];
converter.b[2] = b[1];
converter.b[1] = b[2];
converter.b[0] = b[3];
return converter.f;
}
void setup() {
Serial.begin(115200);
Serial.println("ESP32 Web Server for Vertiv CAN Control");
// Initialize MCP2515 running at 8MHz with a baudrate of 125kb/s and the masks and filters disabled.
if(CAN0.begin(MCP_ANY, CAN_125KBPS, MCP_8MHZ) == CAN_OK) {
Serial.println("MCP2515 Initialized Successfully!");
} else {
Serial.println("Error Initializing MCP2515...");
while (1);
}
// The Adafruit library's begin() function handles setting the bitrate.
Serial.println("CAN init OK!");
// Set to normal mode to allow messages to be transmitted.
CAN0.setMode(MCP_NORMAL);
// --- WiFi Setup ---
if (WIFI_AP_MODE) {
WiFi.softAP(ap_ssid, ap_password);
Serial.print("Access Point created! IP Address: ");
Serial.println(WiFi.softAPIP());
} else {
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.print("Connected to WiFi! IP Address: ");
Serial.println(WiFi.localIP());
}
// --- Web Server Routes Setup ---
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send_P(200, "text/html", html_page);
});
server.on("/data", HTTP_GET, [](AsyncWebServerRequest *request){
String jsonResponse = "{\"outputVoltage\":";
jsonResponse += String(outputVoltage, 2);
jsonResponse += ",\"outputCurrent\":";
jsonResponse += String(outputCurrent, 2);
jsonResponse += ",\"outputCurrentLimit\":";
jsonResponse += String(outputCurrentLimit, 2);
jsonResponse += ",\"temperature\":";
jsonResponse += String(temperature, 2);
jsonResponse += ",\"supplyVoltage\":";
jsonResponse += String(supplyVoltage, 2);
jsonResponse += ",\"isCommandPending\":";
jsonResponse += isCommandPending ? "true" : "false";
jsonResponse += ",\"remainingTime\":";
// Calculate the remaining time in seconds for the countdown
if (isCommandPending) {
unsigned long elapsedTime = millis() - commandSentTime;
long remaining = (long)PERMANENT_COMMAND_DELAY - (long)elapsedTime;
jsonResponse += String(remaining > 0 ? remaining / 1000 : 0);
} else {
jsonResponse += "0";
}
jsonResponse += "}";
request->send(200, "application/json", jsonResponse);
});
server.on("/set_perm_v", HTTP_POST, [](AsyncWebServerRequest *request){
float voltage = request->getParam(0)->value().toFloat();
if (voltage > 0) {
setVertivVoltagePermanent(voltage);
// Dynamic alert message
String message = "Command sent: set_perm_v " + String(voltage) + ". Please wait " + String(PERMANENT_COMMAND_DELAY / 1000) + " seconds for confirmation.";
request->send(200, "text/plain", message);
} else {
request->send(400, "text/plain", "Invalid voltage value.");
}
});
server.on("/set_online_v", HTTP_POST, [](AsyncWebServerRequest *request){
float voltage = request->getParam(0)->value().toFloat();
if (voltage > 0) {
setVertivVoltageOnline(voltage);
request->send(200, "text/plain", "Command sent: set_online_v " + String(voltage));
} else {
request->send(400, "text/plain", "Invalid voltage value.");
}
});
server.on("/set_perm_c", HTTP_POST, [](AsyncWebServerRequest *request){
float currentPercentage = request->getParam(0)->value().toFloat();
if (currentPercentage >= 0.1 && currentPercentage <= 1.21) {
setVertivCurrentPermanent(currentPercentage);
// Dynamic alert message
String message = "Command sent: set_perm_c " + String(currentPercentage) + ". Please wait " + String(PERMANENT_COMMAND_DELAY / 1000) + " seconds for confirmation.";
request->send(200, "text/plain", message);
} else {
request->send(400, "text/plain", "Invalid current percentage.");
}
});
server.on("/set_online_c", HTTP_POST, [](AsyncWebServerRequest *request){
float currentPercentage = request->getParam(0)->value().toFloat();
if (currentPercentage >= 0.1 && currentPercentage <= 1.21) {
setVertivCurrentOnline(currentPercentage);
request->send(200, "text/plain", "Command sent: set_online_c " + String(currentPercentage));
} else {
request->send(400, "text/plain", "Invalid current percentage.");
}
});
server.on("/set_fan_speed", HTTP_POST, [](AsyncWebServerRequest *request){
if (request->hasParam("speed", true)) {
String speed = request->getParam("speed", true)->value();
if (speed == "full") {
setVertivFanSpeed(true);
String message = "Command sent: set fan to full speed. Please wait " + String(PERMANENT_COMMAND_DELAY / 1000) + " seconds for confirmation.";
request->send(200, "text/plain", message);
} else if (speed == "auto") {
setVertivFanSpeed(false);
String message = "Command sent: set fan to auto. Please wait " + String(PERMANENT_COMMAND_DELAY / 1000) + " seconds for confirmation.";
request->send(200, "text/plain", message);
} else {
request->send(400, "text/plain", "Invalid fan speed command.");
}
} else {
request->send(400, "text/plain", "Missing fan speed parameter.");
}
});
server.on("/set_walk_in", HTTP_POST, [](AsyncWebServerRequest *request){
if (request->hasParam("state", true)) {
String state = request->getParam("state", true)->value();
if (state == "on") {
setVertivWalkIn(true);
String message = "Command sent: set walk-in to ON. Please wait " + String(PERMANENT_COMMAND_DELAY / 1000) + " seconds for confirmation.";
request->send(200, "text/plain", message);
} else if (state == "off") {
setVertivWalkIn(false);
String message = "Command sent: set walk-in to OFF. Please wait " + String(PERMANENT_COMMAND_DELAY / 1000) + " seconds for confirmation.";
request->send(200, "text/plain", message);
} else {
request->send(400, "text/plain", "Invalid walk-in state command.");
}
} else {
request->send(400, "text/plain", "Missing walk-in state parameter.");
}
});
server.on("/set_walk_in_time", HTTP_POST, [](AsyncWebServerRequest *request){
float seconds = request->getParam(0)->value().toFloat();
if (seconds >= 0) {
setVertivWalkInTime(seconds);
String message = "Command sent: set walk-in time to " + String(seconds) + ". Please wait " + String(PERMANENT_COMMAND_DELAY / 1000) + " seconds for confirmation.";
request->send(200, "text/plain", message);
} else {
request->send(400, "text/plain", "Invalid walk-in time value.");
}
});
// Start the web server
server.begin();
// Initial request for data from the power supply
readVertivSetting(OUTPUT_VOLTAGE);
readVertivSetting(OUTPUT_CURRENT);
readVertivSetting(OUTPUT_CURRENT_LIMIT);
readVertivSetting(TEMPERATURE);
readVertivSetting(SUPPLY_VOLTAGE);
}
// State machine variables for sequenced reading
unsigned long lastRequestTime = 0;
int readState = 0; // 0: Idle, 1: Request sent, 2: Waiting for 1s delay
const unsigned long POLLING_INTERVAL = 5000;
const unsigned long CURRENT_LIMIT_DELAY = 1000; // 1 second delay
void loop() {
// Check for incoming CAN messages from the power supply
processIncomingCanMessages();
// Sequentially request new data if no command is pending
if (!isCommandPending) {
switch (readState) {
case 0: // Check if it's time to start the sequence
if (millis() - lastRequestTime > POLLING_INTERVAL) {
readVertivSetting(OUTPUT_VOLTAGE);
readState = 1;
lastRequestTime = millis();
}
break;
case 1: // Request OUTPUT_CURRENT, then set a 1-second delay
if (millis() - lastRequestTime > 100) { // Small delay between requests
readVertivSetting(OUTPUT_CURRENT);
readVertivSetting(OUTPUT_CURRENT_LIMIT);
readState = 2;
lastRequestTime = millis();
}
break;
case 2: // Wait for 1 second after requesting current limit
if (millis() - lastRequestTime > CURRENT_LIMIT_DELAY) {
readVertivSetting(TEMPERATURE);
readVertivSetting(SUPPLY_VOLTAGE);
readState = 0; // Reset state for the next cycle
lastRequestTime = millis(); // Reset main polling timer
}
break;
}
}
// Check if the fixed delay for the permanent command has elapsed
if (isCommandPending && millis() - commandSentTime > PERMANENT_COMMAND_DELAY) {
Serial.println("45-second command delay complete. Resuming normal operation.");
isCommandPending = false;
readState = 0; // Reset the read state machine
}
}
/**
* @brief Sends a CAN message to set the output voltage of the Vertiv R48-2000e3 permanently.
* @param voltage The desired voltage in Volts (float).
*
* This function constructs the 8-byte data payload:
* [0x03, 0xF0, 0x00, 0x24, (4 bytes IEEE 754 float)]
* The float voltage is converted to its 4-byte IEEE 754 single-precision representation.
*/
void setVertivVoltagePermanent(float voltage) {
byte data[8];
data[0] = 0x03;
data[1] = 0xF0;
data[2] = 0x00;
data[3] = SET_PERMANENT_VOLTAGE_CMD;
union FloatBytes { float f; byte b[4]; } converter;
converter.f = voltage;
data[4] = converter.b[3];
data[5] = converter.b[2];
data[6] = converter.b[1];
data[7] = converter.b[0];
byte sndStat = CAN0.sendMsgBuf(VERTIV_COMMAND_ID, 1, 8, data);
if (sndStat == CAN_OK) {
Serial.print("Sent permanent voltage command. Value: "); Serial.println(voltage);
// Set command pending flag and start the timer
isCommandPending = true;
commandSentTime = millis();
} else {
Serial.println("Error sending voltage command.");
}
}
/**
* @brief Sends a CAN message to set the output voltage of the Vertiv R48-2000e3 temporarily (online).
* @param voltage The desired voltage in Volts (float).
*
* This function constructs the 8-byte data payload:
* [0x03, 0xF0, 0x00, 0x21, (4 bytes IEEE 754 float)]
*/
void setVertivVoltageOnline(float voltage) {
byte data[8];
data[0] = 0x03;
data[1] = 0xF0;
data[2] = 0x00;
data[3] = 0x21;
union FloatBytes { float f; byte b[4]; } converter;
converter.f = voltage;
data[4] = converter.b[3];
data[5] = converter.b[2];
data[6] = converter.b[1];
data[7] = converter.b[0];
byte sndStat = CAN0.sendMsgBuf(VERTIV_COMMAND_ID, 1, 8, data);
if (sndStat == CAN_OK) {
Serial.print("Sent online voltage command. Value: "); Serial.println(voltage);
} else {
Serial.println("Error sending voltage command.");
}
}
/**
* @brief Sends a CAN message to set the output current limit of the Vertiv R48-2000e3 permanently.
* @param currentPercentage The desired current as a percentage of rated value (e.g., 0.1 for 10%, 1.21 for 121%).
*
* This function constructs the 8-byte data payload:
* [0x03, 0xF0, 0x00, 0x19, (4 bytes IEEE 754 float)]
*/
void setVertivCurrentPermanent(float currentPercentage) {
byte data[8];
data[0] = 0x03;
data[1] = 0xF0;
data[2] = 0x00;
data[3] = SET_PERMANENT_CURRENT_LIMIT_CMD;
union FloatBytes { float f; byte b[4]; } converter;
converter.f = currentPercentage;
data[4] = converter.b[3];
data[5] = converter.b[2];
data[6] = converter.b[1];
data[7] = converter.b[0];
byte sndStat = CAN0.sendMsgBuf(VERTIV_COMMAND_ID, 1, 8, data);
if (sndStat == CAN_OK) {
Serial.print("Sent permanent current limit command. Value: "); Serial.println(currentPercentage);
// Set command pending flag and start the timer
isCommandPending = true;
commandSentTime = millis();
} else {
Serial.println("Error sending current command.");
}
}
/**
* @brief Sends a CAN message to set the output current limit of the Vertiv R48-2000e3 online.
* @param currentPercentage The desired current as a percentage of rated value (e.g., 0.1 for 10%, 1.21 for 121%).
*
* This function constructs the 8-byte data payload:
* [0x03, 0xF0, 0x00, 0x22, (4 bytes IEEE 754 float)]
*/
void setVertivCurrentOnline(float currentPercentage) {
byte data[8];
data[0] = 0x03;
data[1] = 0xF0;
data[2] = 0x00;
data[3] = 0x22;
union FloatBytes { float f; byte b[4]; } converter;
converter.f = currentPercentage;
data[4] = converter.b[3];
data[5] = converter.b[2];
data[6] = converter.b[1];
data[7] = converter.b[0];
byte sndStat = CAN0.sendMsgBuf(VERTIV_COMMAND_ID, 1, 8, data);
if (sndStat == CAN_OK) {
Serial.print("Sent online current limit command. Value: "); Serial.println(currentPercentage);
} else {
Serial.println("Error sending current command.");
}
}
/**
* @brief Sends a CAN message to request a specific measurement from the Vertiv R48-2000e3.
* @param measurementNo The measurement number to request (e.g., 0x01 for output voltage).
*
* Request format: Send to 0x06000783 => [0x01, 0xF0, 0x00, xx, 0x00, 0x00, 0x00, 0x00]
*/
void readVertivSetting(byte measurementNo) {
byte data[8];
data[0] = 0x01;
data[1] = 0xF0;
data[2] = 0x00;
data[3] = measurementNo;
data[4] = 0x00;
data[5] = 0x00;
data[6] = 0x00;
data[7] = 0x00;
byte sndStat = CAN0.sendMsgBuf(VERTIV_READ_REQUEST_ID, 1, 8, data);
if (sndStat == CAN_OK) {
Serial.print("Sent read request command. Measurement #: "); Serial.println(measurementNo, HEX);
} else {
Serial.println("Error requesting measurement.");
}
}
/**
* @brief Sends a CAN message to set the fan speed.
* @param fullSpeed A boolean flag: true for full speed, false for auto.
*/
void setVertivFanSpeed(bool fullSpeed) {
byte data[8];
data[0] = 0x03;
data[1] = 0xF0;
data[2] = 0x00;
data[3] = 0x33;
data[4] = fullSpeed ? 0x01 : 0x00;
data[5] = 0x00;
data[6] = 0x00;
data[7] = 0x00;
byte sndStat = CAN0.sendMsgBuf(VERTIV_COMMAND_ID, 1, 8, data);
if (sndStat == CAN_OK) {
Serial.print("Sent fan speed command. Value: ");
Serial.println(fullSpeed ? "Full Speed" : "Auto");
// Set command pending flag and start the timer
isCommandPending = true;
commandSentTime = millis();
} else {
Serial.println("Error sending fan speed command.");
}
}
/**
* @brief Sends a CAN message to enable or disable the walk-in feature.
* @param on A boolean flag: true to enable walk-in, false to disable.
*/
void setVertivWalkIn(bool on) {
byte data[8];
data[0] = 0x03;
data[1] = 0xF0;
data[2] = 0x00;
data[3] = 0x32;
data[4] = on ? 0x01 : 0x00;
data[5] = 0x00;
data[6] = 0x00;
data[7] = 0x00;
byte sndStat = CAN0.sendMsgBuf(VERTIV_COMMAND_ID, 1, 8, data);
if (sndStat == CAN_OK) {
Serial.print("Sent walk-in command. Value: ");
Serial.println(on ? "On" : "Off");
// Set command pending flag and start the timer
isCommandPending = true;
commandSentTime = millis();
} else {
Serial.println("Error sending walk-in command.");
}
}
/**
* @brief Sends a CAN message to set the walk-in ramp-up time.
* @param seconds The desired ramp-up time in seconds (float).
*/
void setVertivWalkInTime(float seconds) {
byte data[8];
data[0] = 0x03;
data[1] = 0xF0;
data[2] = 0x00;
data[3] = 0x29;
union FloatBytes { float f; byte b[4]; } converter;
converter.f = seconds;
data[4] = converter.b[3];
data[5] = converter.b[2];
data[6] = converter.b[1];
data[7] = converter.b[0];
byte sndStat = CAN0.sendMsgBuf(VERTIV_COMMAND_ID, 1, 8, data);
if (sndStat == CAN_OK) {
Serial.print("Sent walk-in time command. Value: ");
Serial.println(seconds);
// Set command pending flag and start the timer
isCommandPending = true;
commandSentTime = millis();
} else {
Serial.println("Error sending walk-in time command.");
}
}
/**
* @brief Checks for and processes incoming CAN messages from the Vertiv R48-2000e3.
*/
void processIncomingCanMessages() {
long unsigned int rxId;
unsigned char len;
unsigned char rxBuf[8];
// Check if a message has been received
if(CAN0.checkReceive() == CAN_MSGAVAIL) {
// Read the message
CAN0.readMsgBuf(&rxId, &len, rxBuf);
// Log every received message to the Serial Monitor
Serial.print("RX ID: 0x");
if (rxId < 0x10000000) Serial.print("0"); // Manual padding for 32-bit ID
Serial.print(rxId, HEX);
Serial.print(" Length: ");
Serial.print(len);
Serial.print(" Data: ");
for (int i = 0; i < len; i++) {
Serial.print("0x");
if (rxBuf[i] < 0x10) Serial.print("0");
Serial.print(rxBuf[i], HEX);
Serial.print(" ");
}
Serial.println();
// Parse the message if it's a standard Vertiv response
if (rxId == VERTIV_RESPONSE_ID && len == 8 && rxBuf[0] == 0x41 && rxBuf[1] == 0xF0 && rxBuf[2] == 0x00) {
byte receivedMeasurementNo = rxBuf[3];
// Create a temporary buffer for the float bytes from the CAN message
byte floatBytes[4] = {rxBuf[4], rxBuf[5], rxBuf[6], rxBuf[7]};
float receivedValue = bytesToFloat(floatBytes);
// Log the converted value to the serial monitor for debugging
Serial.print("Parsed Value: ");
Serial.println(receivedValue, 2);
// Update global variables
switch (receivedMeasurementNo) {
case OUTPUT_VOLTAGE: outputVoltage = receivedValue; break;
case OUTPUT_CURRENT: outputCurrent = receivedValue; break;
case OUTPUT_CURRENT_LIMIT: outputCurrentLimit = receivedValue; break;
case TEMPERATURE: temperature = receivedValue; break;
case SUPPLY_VOLTAGE: supplyVoltage = receivedValue; break;
}
}
}
}




#pragma once
#include <stdint.h>
#include <stdbool.h>
#include <soc/gpio_num.h>
#define R48_MAX_DEVICE_COUNT 2
#define R48_TEMPERATURE_OFFSET 6.2f // Gemessene Temperatur +5°C (Kalibrierung)
#define R48_MIN_STATUS_UPDATE_INTERVAL_MS 2000UL // Minimaler Abstand zwischen Status-Updates
#define R48_RATED_CURRENT_A (62.5f)
#define R48_MIN_VOLTAGE_V (41.0f)
#define R48_MAX_VOLTAGE_V (58.0f)
#define R48_DEFAULT_OFFLINE_CURRENT_A (0.0f) // Default offline current to set on startup if stored value not found
#define R48_DEFAULT_OFFLINE_VOLTAGE_V (48.0f) // Default offline voltage to set on startup if stored value not found
//Allowed range 0xF0..0xF8
//This is a special internal address that is coded into the CAN IDs for the R48 protocol
#define R48_THIS_DEVICE_ADDRESS (0xF0) // Address of this controller
#define R48_BROADCAST_ADDRESS (0xFF) // Broadcast address for all devices
//Type definitions
#define R48_DATA_ERRTYPE_NONE 0xF0
#define R48_DATA_ERRTYPE_INVALID_ADR 0xF1
#define R48_DATA_ERRTYPE_INVALID_CMD 0xF2
#define R48_DATA_ERRTYPE_INVALID_DATA 0xF3
#define R48_DATA_ERRTYPE_ADR_IDENT_RUNNING 0xF4
//MSGTYPE definitions
#define R48_MSGTYPE_REQUEST_ALL 0x00
#define R48_MSGTYPE_REQUEST_BYTE_DATA 0x01
#define R48_MSGTYPE_RESPONSE_BYTE_DATA 0x41
#define R48_MSGTYPE_REQUEST_BIT_DATA 0x02
#define R48_MSGTYPE_RESPONSE_BIT_DATA 0x42
#define R48_MSGTYPE_WRITE_DATA 0x03
#define R48_MSGTYPE_WRITE_DATA_ACK 0x43
#define R48_PROTO_MODULE_CONTROLLER_COMM 0x060
#define R48_IOUT_OFFLINE_NVS_KEY "r48IoutOff"
#define R48_VOUT_OFFLINE_NVS_KEY "r48VoutOff"
#ifdef __cplusplus
extern "C" {
#endif
// Address of a R48 module
typedef uint8_t r48_module_adr_t;
typedef struct r48_can_id_t
{
//Adress is in this format:
/*
• Bits 28-20: Protokollnummer (PROTNO, 9 bits)
• Bit 19: PTP (Point-to-Point Indikator)
• Bits 18-11: Zieladresse (DSTADDR, 8 bits)
• Bits 10-3: Quelladresse (SRCADDR, 8 bits)
• Bits 2-0: CNT, RES1, RES2 (Zähler / Reserviert)
*/
union
{
struct
{
uint32_t direct;
} u32;
struct
{
uint32_t res2:1; //allways 1
uint32_t res1:1; //allways 1
uint32_t cnt:1; //0 if last frame from this source, 1 if more frames are comming
uint32_t src_addr:8; // Source Address range 0x00-0xFE 0x00-0x7f are from modules, 0xF0-0xF8 for master controllers
uint32_t dst_addr:8; //Same as src_addr
uint32_t ptp:1; // Point-to-Point indicator if broadcast 0 than dest_addr is 0xFF
uint32_t proto:9; // allways 0x060 for R48
} bits;
};
} r48_can_id_t;
typedef enum r48_valuetype_e
{
R48_VALUETYPE_VOUT = 0x01,
R48_VALUETYPE_IOUT = 0x02,
R48_VALUETYPE_ILIMIT = 0x03,
R48_VALUETYPE_TEMPERATURE = 0x04,
R48_VALUETYPE_VIN = 0x05,
R48_VALUETYPE_INVALID = 0x06,
R48_VALUETYPE_DISPLAY_CURRENT = 0x07,
R48_VALUETYPE_ROOM_TEMPERATURE = 0x0B,
R48_VALUETYPE_IOUT_OFFLINE = 0x19,
R48_VALUETYPE_AC_INPUT_ILIMIT = 0x1A, // "Diesel power limit" = AC input current limit
R48_VALUETYPE_VOUT_ONLINE = 0x21,
R48_VALUETYPE_IOUT_ONLINE = 0x22,
R48_VALUETYPE_VOUT_OFFLINE = 0x24,
R48_VALUETYPE_WALKIN_TIME = 0x29, // Ramp-up time (sec)
R48_VALUETYPE_REMOTE_OFF = 0x30,
R48_VALUETYPE_WALKIN_ENABLE = 0x32, // Walk-in on/off
R48_VALUETYPE_FAN_CONTROL = 0x33, // 0x00 = auto, 0x01 = full speed
R48_VALUETYPE_TURN_OFF_MODULE = 0x35,
R48_VALUETYPE_RESTART_AFTER_OV = 0x39, // Restart after overvoltage (on/off)
R48_VALUETYPE_BIT_STATUS = 0x40,
R48_VALUETYPE_RESERVED_1 = 0x54,
R48_VALUETYPE_RESERVED_2 = 0x58,
R48_VALUETYPE_ALL = 0x80,
} r48_valuetype_e;
typedef struct r48_data_t
{
uint8_t msgtype:7;
uint8_t err:1;
uint8_t errType;
union
{
uint8_t all_bytes[6];
struct {
uint8_t valuetype0_allways0; // At least i think it is always 0x00
uint8_t valuetype1; // r48_get_request_e
uint8_t content[4];
} byte_wise;
} data;
} r48_data_t;
typedef struct r48_device_mem_t
{
float v_out_set; // Gesetzte Ausgangsspannung
int64_t v_out_set_timestamp;
float i_out_set; // Gesetzter Ausgangsstrom
int64_t i_out_set_timestamp;
//Gemessene Werte
float v_out; // Ausgangsspannung
int64_t v_out_timestamp;
float i_out; // Ausgangsstrom
int64_t i_out_timestamp;
float temp_C; // Temperatur in °C
float room_temp_C; // Temperatur in °C
float v_in; // Eingangsspannung
float i_limit; // Wrong data
float v_out_offline; // Letzte gesetzte offline Spannung
float i_out_offline; // Letzter gesetzter offline Strom
union {
struct {
uint8_t byte3;
uint8_t byte2;
uint8_t byte1;
uint8_t byte0;
};
struct {
//Data Byte 4
uint8_t temp_limited_power:1;
uint8_t ac_power_limitation:1;
uint8_t eeprom_module_error:1;
uint8_t fan_failure:1;
uint8_t module_protection:1;
uint8_t module_fault:1;
uint8_t overtemperature:1;
uint8_t overvoltage:1;
//Data Byte 5
uint8_t can_error_status:1;
uint8_t module_power_balance_error:1;
uint8_t module_identification:1;
uint8_t overvoltage_shutdown_relay:1;
uint8_t walk_in_function:1;
uint8_t fan_full_speed:1;
uint8_t module_switch:1;
uint8_t module_power_limit:1;
//Data Byte 6
uint8_t module_pfc_error:1;
uint8_t module_ac_overvoltage:1;
uint8_t duplicate_module_id:1;
uint8_t severe_current_imbalance:1;
uint8_t comm_phase_loss:1;
uint8_t comm_imbalance:1;
uint8_t module_ac_undervoltage_alarm:1;
uint8_t sequential_start_function:1;
//Data Byte 7
uint8_t reserved0:1;
uint8_t reserved1:1;
uint8_t fuse_fault_output:1;
uint8_t internal_comm_error:1;
uint8_t undervoltage_output:1;
uint8_t overload_alarm:1;
uint8_t pnp_alarm:1;
uint8_t slight_current_imbalance:1;
} bits;
uint8_t bytes[4];
uint32_t u32;;
} bit_status; // Status-Bits
bool remote_off; // Ob das Gerät per Remote-Off abgeschaltet ist
// int64_t last_online_update; // TickCount des letzten Updates
// int64_t last_request_time; // Zeit des letzten Requests
} r48_device_mem_t;
typedef struct r48_device_mem_t* r48_handle_t;
typedef struct r48_mem_t
{
int64_t last_status_broadcast_req_time;
r48_handle_t devices[R48_MAX_DEVICE_COUNT];
} r48_mem_t;
extern r48_mem_t r48_mem;
bool r48_init(gpio_num_t tx_gpio, gpio_num_t rx_gpio);
void r48_worker(void);
bool r48_set_current_offline(r48_module_adr_t module_adr, float amps);
bool r48_set_current_online(r48_module_adr_t module_adr, float amps);
bool r48_set_voltage_offline(r48_module_adr_t module_adr, float volts);
bool r48_set_voltage_online(r48_module_adr_t module_adr, float volts);
bool r48_fan_set_fullspeed(r48_module_adr_t module_adr);
bool r48_fan_set_auto(r48_module_adr_t module_adr);
bool r48_remote_on(r48_module_adr_t module_adr);
bool r48_remote_off(r48_module_adr_t module_adr);
bool r48_turn_on_off_module(bool on);
bool r48_send_status_request(r48_module_adr_t dest_adr, r48_valuetype_e valuetype);
#ifdef __cplusplus
}
#endif
#include "R48.h"
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <esp_log.h>
#include <esp_err.h>
#include <string.h>
#include <math.h>
#include <driver/twai.h> // legacy TWAI API (OK in IDF 5.5)
#include <esp_check.h>
#include <esp_timer.h>
#include <esp_heap_caps.h>
#include <bareco_database.h>
_Static_assert(sizeof(r48_data_t) == 8, "r48_data_t must be exactly 8 bytes");
_Static_assert(sizeof(((r48_device_mem_t*)0)->bit_status) == 4, "bit_status must be 4 bytes");
const static char TAG[] = "R48";
r48_mem_t r48_mem = {0};
static inline float be_to_float(const uint8_t in[4]);
static inline float fraction_to_amps(float frac);
static void r48_log_frame(const twai_message_t *msg)
{
char buf[3 * 8 + 1] = {0};
int n = 0;
for(int i = 0; i < msg->data_length_code && i < 8; ++i)
{
n += snprintf(&buf[n], sizeof(buf) - n, "%02X ", msg->data[i]);
}
ESP_LOGI(TAG,
"RX %s ID=0x%08X DLC=%d%s%s data=[%s]",
msg->extd ? "EXT" : "STD",
msg->identifier,
msg->data_length_code,
msg->rtr ? " RTR" : "",
#if CONFIG_IDF_TARGET_ESP32 || CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C3
msg->self ? " SELF" : "",
#else
"",
#endif
buf);
}
void r48_log_msg_full_details(const twai_message_t *msg)
{
ESP_LOGI(TAG, "R48 Message Details:");
const r48_can_id_t *can_id = (const r48_can_id_t *)&msg->identifier;
ESP_LOGI(TAG, " Protocol: 0x%03X", can_id->bits.proto);
ESP_LOGI(TAG, " PTP: %u", can_id->bits.ptp);
ESP_LOGI(TAG, " Dest Addr: 0x%02X", can_id->bits.dst_addr);
ESP_LOGI(TAG, " Src Addr: 0x%02X", can_id->bits.src_addr);
ESP_LOGI(TAG, " CNT: %u", can_id->bits.cnt);
if(!can_id->bits.res1 || !can_id->bits.res2)
{
ESP_LOGW(TAG, " Warning: RES1 or RES2 bits are not set to 1 as expected!");
}
const r48_data_t *data = (const r48_data_t *)msg->data;
ESP_LOGI(TAG, " Message Type: 0x%02X", data->msgtype);
ESP_LOGI(TAG, " Error Flag: %u", data->err);
ESP_LOGI(TAG, " Error Type: 0x%02X", data->errType);
char buffer[4 * 6 + 1] = {0};
for(int i = 0; i < 4; i++)
{
sprintf(&buffer[i * 5], "0x%02X ", data->data.byte_wise.content[i]);
}
ESP_LOGI(TAG, " Content Bytes: %s", buffer);
ESP_LOGI(TAG, " Content as Float: %.3f", be_to_float(data->data.byte_wise.content));
}
static void print_all_status_flag_set(r48_module_adr_t addr)
{
r48_handle_t handle = r48_mem.devices[addr];
if(handle == NULL || addr >= R48_MAX_DEVICE_COUNT)
{
ESP_LOGW(TAG, "print_all_status_flag_set: No handle for device index 0x%02X", addr);
return;
}
const char *flags_names[32] = {
"TEMP_LIMITED_POWER",
"AC_POWER_LIMITATION",
"EEPROM_MODULE_ERROR",
"FAN_FAILURE",
"MODULE_PROTECTION",
"MODULE_FAULT",
"OVERTEMPERATURE",
"OVERVOLTAGE",
"CAN_ERROR_STATUS",
"MODULE_POWER_BALANCE_ERROR",
"MODULE_IDENTIFICATION",
"OVERVOLTAGE_SHUTDOWN_RELAY",
"WALK_IN_FUNCTION",
"FAN_FULL_SPEED",
"MODULE_SWITCH",
"MODULE_POWER_LIMIT",
"MODULE_PFC_ERROR",
"MODULE_AC_OVERVOLTAGE",
"DUPLICATE_MODULE_ID",
"SEVERE_CURRENT_IMBALANCE",
"COMM_PHASE_LOSS",
"COMM_IMBALANCE",
"MODULE_AC_UNDERVOLTAGE_ALARM",
"SEQUENTIAL_START_FUNCTION",
"RESERVED0",
"RESERVED1",
"FUSE_FAULT_OUTPUT",
"INTERNAL_COMM_ERROR",
"UNDERVOLTAGE_OUTPUT",
"OVERLOAD_ALARM",
"PNP_ALARM",
"SLIGHT_CURRENT_IMBALANCE"
};
for(int i = 0; i < 32; i++)
{
if(handle->bit_status.u32 & (1 << i))
{
ESP_LOGI(TAG, "R48[0x%02X]: Status Flag set: %s", addr, flags_names[i]);
}
}
ESP_LOGI(TAG, "MODULE_SWITCH=%s", handle->bit_status.bits.module_switch ? "ON" : "OFF");
}
static inline float be_to_float(const uint8_t in[4])
{
union { float f; uint8_t b[4]; } u;
u.b[3] = in[0];
u.b[2] = in[1];
u.b[1] = in[2];
u.b[0] = in[3];
return u.f;
}
static inline void float_to_be(float f, uint8_t out[4])
{
union { float f; uint8_t b[4]; } u;
u.f = f;
out[0] = u.b[3];
out[1] = u.b[2];
out[2] = u.b[1];
out[3] = u.b[0];
}
bool r48_init(gpio_num_t tx_gpio, gpio_num_t rx_gpio)
{
// General config
twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT(tx_gpio, rx_gpio, TWAI_MODE_NORMAL);
g_config.tx_queue_len = 10;
g_config.rx_queue_len = 10;
g_config.alerts_enabled = 0; // optional: TWAI_ALERT_TX_SUCCESS | TWAI_ALERT_BUS_OFF ...
// 125 kbps timing + accept all filter
twai_timing_config_t t_config = TWAI_TIMING_CONFIG_125KBITS();
twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL();
esp_err_t ret = twai_driver_install(&g_config, &t_config, &f_config);
if(ret != ESP_OK)
{
ESP_LOGE(TAG, "twai_driver_install failed: %s", esp_err_to_name(ret));
return false;
}
ret = twai_start();
if(ret != ESP_OK)
{
ESP_LOGE(TAG, "twai_start failed: %s", esp_err_to_name(ret));
return false;
}
ESP_LOGI(TAG, "TWAI started @125kbps, ext frames");
// for(uint8_t i = 0; i < R48_MAX_DEVICE_COUNT; i++)
// {
// if(r48_devices[i] == NULL)
// {
// r48_device_mem_t *mem = heap_caps_calloc(1, sizeof(r48_device_mem_t), MALLOC_CAP_DEFAULT | MALLOC_CAP_INTERNAL);
// if(mem == NULL)
// {
// ESP_LOGE(TAG, "malloc failed");
// return NULL;
// }
// mem->device_index = i;
// r48_devices[i] = mem;
// return mem;
// }
// }
ESP_LOGI(TAG, "R48 initialized");
return true;
}
static bool r48_parse_status_message(const twai_message_t *msg)
{
r48_data_t data;
memcpy(&data, msg->data, sizeof(data));
if(data.errType != R48_DATA_ERRTYPE_NONE)
{
ESP_LOGW(TAG, "R48: Error in status message, errType=0x%02X", data.errType);
r48_log_frame(msg);
return false;
}
if(data.msgtype != R48_MSGTYPE_RESPONSE_BYTE_DATA && data.msgtype != R48_MSGTYPE_RESPONSE_BIT_DATA && data.msgtype != R48_MSGTYPE_WRITE_DATA_ACK)
{
ESP_LOGW(TAG, "R48: Unknown message type 0x%02X in status message", data.msgtype);
//On module on/off:
/*
W (18563) R48: R48: Unknown message type 0x43 in status message
I (18563) R48: RX EXT ID=0x060F8003 DLC=8 data=[43 F0 00 35 00 00 00 00 ]
*/
r48_log_frame(msg);
return false;
}
r48_can_id_t addr = {.u32.direct = msg->identifier};
r48_handle_t handle = r48_mem.devices[addr.bits.src_addr];
if(handle == NULL)
{
ESP_LOGW(TAG, "r48_parse_status_message: No handle for device index 0x%02X", msg->identifier & 0xFF);
return false;
}
float float_value = be_to_float(data.data.byte_wise.content);
switch(data.data.byte_wise.valuetype1)
{
case R48_VALUETYPE_VOUT: // VOUT
handle->v_out = float_value;
handle->v_out_timestamp = esp_timer_get_time();
ESP_LOGI(TAG, "R48: VOUT = %.2f V", handle->v_out);
return true;
case R48_VALUETYPE_IOUT: // IOUT
handle->i_out = float_value;
handle->i_out_timestamp = esp_timer_get_time();
ESP_LOGI(TAG, "R48: IOUT = %.2f A", handle->i_out);
return true;
case R48_VALUETYPE_ILIMIT: // ILIMIT
// Currently ignored
ESP_LOGI(TAG, "R48: ILIMIT message received: %f A", float_value);
return true;
case R48_VALUETYPE_TEMPERATURE: // TEMPERATURE
handle->temp_C = float_value + R48_TEMPERATURE_OFFSET;
ESP_LOGI(TAG, "R48: TEMPERATURE = %.2f °C", handle->temp_C);
return true;
case R48_VALUETYPE_VIN: // VIN
handle->v_in = float_value;
ESP_LOGI(TAG, "R48: VIN = %.2f V", handle->v_in);
return true;
case R48_VALUETYPE_BIT_STATUS: // BIT STATUS
memcpy(handle->bit_status.bytes, msg->data + 4, 4);
ESP_LOGI(TAG, "R48: BIT STATUS = 0x%08X", handle->bit_status.u32);
print_all_status_flag_set(addr.bits.src_addr);
return true;
case R48_VALUETYPE_IOUT_OFFLINE:
{
float amps = fraction_to_amps(float_value);
handle->i_out_offline = amps;
char key[16];
snprintf(key, sizeof(key), "%s%02X", R48_IOUT_OFFLINE_NVS_KEY, addr.bits.src_addr);
esp_err_t ret = bareco_database_set_float(key, amps);
if(ret != ESP_OK)
{
ESP_LOGW(TAG, "r48_set_current_offline: Failed to store offline current in NVS for module 0x%02X: %s", addr.bits.src_addr, esp_err_to_name(ret));
}
ESP_LOGI(TAG, "R48: IOUT_OFFLINE = %.2f A", handle->i_out_offline);
return true;
}
case R48_VALUETYPE_VOUT_OFFLINE:
{
float volts = float_value; // fraction_to_volts(float_value);
handle->v_out_offline = volts;
char key[16];
snprintf(key, sizeof(key), "%s%02X", R48_VOUT_OFFLINE_NVS_KEY, addr.bits.src_addr);
esp_err_t ret = bareco_database_set_float(key, volts);
if(ret != ESP_OK)
{
ESP_LOGW(TAG, "r48_set_voltage_offline: Failed to store offline voltage in NVS for module 0x%02X: %s", addr.bits.src_addr, esp_err_to_name(ret));
}
ESP_LOGI(TAG, "R48: VOUT_OFFLINE = %.2f V", handle->v_out_offline);
return true;
}
case R48_VALUETYPE_IOUT_ONLINE:
{
float amps = fraction_to_amps(float_value);
handle->i_out_set = amps;
handle->i_out_set_timestamp = esp_timer_get_time();
handle->remote_off = false; //Setting current will enable the output
ESP_LOGI(TAG, "R48: IOUT SET ONLINE = %.2f A", handle->i_out_set);
return true;
}
case R48_VALUETYPE_VOUT_ONLINE:
{
handle->v_out_set = float_value;
handle->v_out_set_timestamp = esp_timer_get_time();
handle->remote_off = false; //Setting voltage will enable the output
ESP_LOGI(TAG, "R48: VOUT SET ONLINE = %.2f V", handle->v_out_set);
return true;
}
case 0x54: //fallthroug // reserved and ignored
case 0x58:
//Currently ignored
return true;
case 0x07: // DISPLAY_CURRENT
// Currently ignored
return true;
case 0x0B: // ROOM_TEMPERATURE
handle->room_temp_C = float_value;
ESP_LOGI(TAG, "R48: ROOM_TEMPERATURE = %.2f °C", handle->room_temp_C);
return true;
default:
ESP_LOGW(TAG, "R48: Unknown valuetype 0x%02X in status message", data.data.byte_wise.valuetype1);
ESP_LOGW(TAG, "R48: Value = %.2f", float_value);
// I (81181) R48: RX EXT ID=0x060F8007 DLC=8 SELF data=[41 F0 00 07 00 00 00 00 ]
r48_log_frame(msg);
return false;
}
}
static bool r48_route_message(const twai_message_t *msg)
{
if(msg->data_length_code != 8)
return false; // Invalid message
r48_can_id_t addr = {.u32.direct = msg->identifier};
if(addr.bits.proto != 0x070 && addr.bits.proto != 0x060)
{
//lets ignore non-R48 messages
return false;
}
uint8_t src_addr = addr.bits.src_addr;
if(src_addr >= R48_MAX_DEVICE_COUNT)
{
ESP_LOGW(TAG, "r48_route_message: Source address 0x%02X out of range", src_addr);
return false;
}
//But we should register a device for this source address if it not exists yet
r48_handle_t handle = r48_mem.devices[src_addr];;
if(handle == NULL)
{
ESP_LOGW(TAG, "r48_route_message: New device detected at address 0x%02X, allocating memory", src_addr);
r48_mem.devices[src_addr] = heap_caps_calloc(1, sizeof(r48_device_mem_t), MALLOC_CAP_DEFAULT | MALLOC_CAP_INTERNAL);
if(r48_mem.devices[src_addr] == NULL)
{
ESP_LOGE(TAG, "r48_route_message: malloc failed for device index 0x%02X", src_addr);
return false;
}
char key[16];
snprintf(key, sizeof(key), "%s%02X", R48_IOUT_OFFLINE_NVS_KEY, src_addr);
esp_err_t ret1 = bareco_database_get_float(key, &r48_mem.devices[src_addr]->i_out_offline);
snprintf(key, sizeof(key), "%s%02X", R48_VOUT_OFFLINE_NVS_KEY, src_addr);
esp_err_t ret2 = bareco_database_get_float(key, &r48_mem.devices[src_addr]->v_out_offline);
if(ret1 != ESP_OK || ret2 != ESP_OK)
{
ESP_LOGW(TAG, "r48_route_message: No stored offline values for device 0x%02X, using defaults", src_addr);
r48_set_current_offline(src_addr, R48_DEFAULT_OFFLINE_CURRENT_A);
r48_set_voltage_offline(src_addr, R48_DEFAULT_OFFLINE_VOLTAGE_V);
}
else
{
ESP_LOGI(TAG, "r48_route_message: Loaded stored offline values for device 0x%02X: Iout=%.2f A, Vout=%.2f V", src_addr,
r48_mem.devices[src_addr]->i_out_offline,
r48_mem.devices[src_addr]->v_out_offline);
r48_set_current_offline(src_addr, r48_mem.devices[src_addr]->i_out_offline);
r48_set_voltage_offline(src_addr, r48_mem.devices[src_addr]->v_out_offline);
}
}
uint8_t dst_addr = addr.bits.dst_addr;
if(dst_addr != 0xFF && dst_addr != R48_THIS_DEVICE_ADDRESS)
{
//Not for us
ESP_LOGI(TAG, "r48_route_message: Message not for us (dst=0x%02X)", dst_addr);
return false;
}
if(addr.bits.proto == 0x070)
{
// device to device message, are ignored
return false;
}
return r48_parse_status_message(msg);
// // Process message
// switch(msg->identifier)
// {
// case 0x0707F803: // Status broadcast
// // ESP_LOGI(TAG, "R48 Status message received");
// // Currently ignored
// return true;
// case 0x060F8007: //fallthrough
// case 0x060F8003:
// {
// return r48_parse_status_message(handle, msg);
// }
// default:
// r48_log_frame(msg);
// return false; // Unknown message
// }
}
bool r48_send_can_message(const twai_message_t *msg, TickType_t ticks_to_wait)
{
esp_err_t ret = twai_transmit(msg, ticks_to_wait);
if(ret != ESP_OK)
{
ESP_LOGE(TAG, "r48_send_can_message: twai_transmit failed: %s", esp_err_to_name(ret));
return false;
}
return true;
}
//Dest address 0xFF = broadcast
bool r48_send_status_request(r48_module_adr_t dest_adr, r48_valuetype_e request)
{
ESP_LOGI(TAG, "r48_send_status_request: Sending request for %02X", request); //, request_text[request] ? request_text[request] : "UNKNOWN");
if(dest_adr != R48_BROADCAST_ADDRESS && dest_adr >= R48_MAX_DEVICE_COUNT)
{
ESP_LOGW(TAG, "r48_send_status_request: Destination address 0x%02X out of range", dest_adr);
return false;
}
r48_can_id_t can_id = {0};
can_id.bits.proto = 0x060; // R48 protocol
can_id.bits.dst_addr = dest_adr;
can_id.bits.src_addr = R48_THIS_DEVICE_ADDRESS;
can_id.bits.ptp = (dest_adr != R48_BROADCAST_ADDRESS) ? 1 : 0;
can_id.bits.cnt = 0;
can_id.bits.res1 = 1;
can_id.bits.res2 = 1;
twai_message_t msg = {0};
msg.identifier = can_id.u32.direct;
msg.extd = 1; // 29-bit ID
msg.rtr = 0;
msg.data_length_code = 8;
switch (request)
{
case R48_VALUETYPE_ILIMIT: //fallthrough
case R48_VALUETYPE_VOUT: //fallthrough
case R48_VALUETYPE_IOUT: //fallthrough
case R48_VALUETYPE_TEMPERATURE: //fallthrough
case R48_VALUETYPE_VIN: //fallthrough
case R48_VALUETYPE_ROOM_TEMPERATURE: //fallthrough
case R48_VALUETYPE_BIT_STATUS: //fallthrough
{
r48_data_t data =
{
.err = 0,
.msgtype = R48_MSGTYPE_REQUEST_BYTE_DATA,
.errType = R48_DATA_ERRTYPE_NONE,
.data.byte_wise = {
.valuetype0_allways0 = 0x00,
.valuetype1 = (uint8_t)request,
.content = {0x00, 0x00, 0x00, 0x00}
}};
memcpy(msg.data, &data, 8);
return r48_send_can_message(&msg, 2); //Should not block long time
}
case R48_VALUETYPE_ALL:
{
r48_data_t data =
{
.err = 0,
.msgtype = R48_MSGTYPE_REQUEST_ALL,
.errType = R48_DATA_ERRTYPE_NONE,
.data.byte_wise = {
.valuetype0_allways0 = 0x00,
.valuetype1 = 0x80, // all
.content = {0x44, 0x7A, 0x00, 0x00} //From example, works also with 0x00 0x00 0x00 0x00
}};
memcpy(msg.data, &data, 8);
return r48_send_can_message(&msg, 2); //Should not block long time
}
default:
ESP_LOGW(TAG, "r48_send_status_request: Unsupported request %" PRIu8, request);
// r48_log_msg_full_details(&msg);
return false;
}
}
void r48_worker(void)
{
twai_message_t msg;
while(twai_receive(&msg, 2) == ESP_OK)
{
r48_route_message(&msg);
}
// // Check for timeouts
int64_t now = esp_timer_get_time();
// if((handle->v_out_timestamp == 0 || now - handle->v_out_timestamp > (R48_STATUS_UPDATE_INTERVAL_MS * 1000UL)))
// {
// Value too old
// #define STOP_STATUS_BROADCAST_REQUESTS 1
if((now - r48_mem.last_status_broadcast_req_time) >= (R48_MIN_STATUS_UPDATE_INTERVAL_MS * 1000UL))
{
#ifndef STOP_STATUS_BROADCAST_REQUESTS
for(uint8_t dev = 0; dev < R48_MAX_DEVICE_COUNT; dev++)
{
while(twai_receive(&msg, 0) == ESP_OK)
{
r48_route_message(&msg);
}
if(r48_mem.devices[dev] == NULL)
{
continue;
}
r48_send_status_request(dev, R48_VALUETYPE_BIT_STATUS);
r48_send_status_request(dev, R48_VALUETYPE_IOUT);
r48_send_status_request(dev, R48_VALUETYPE_VOUT);
r48_send_status_request(dev, R48_VALUETYPE_VIN);
r48_send_status_request(dev, R48_VALUETYPE_ROOM_TEMPERATURE);
r48_send_status_request(dev, R48_VALUETYPE_TEMPERATURE);
}
// if(r48_mem.devices[0] != NULL)
// {
// if(!r48_mem.devices[0]->remote_off)
// r48_send_status_request(R48_BROADCAST_ADDRESS, R48_VALUETYPE_ALL);
// }
//Braodcast requests are not working like this
// r48_send_status_request(R48_BROADCAST_ADDRESS, R48_VALUETYPE_ROOM_TEMPERATURE);
// r48_send_status_request(R48_BROADCAST_ADDRESS, R48_VALUETYPE_BIT_STATUS);
// r48_send_status_request(R48_BROADCAST_ADDRESS, R48_VALUETYPE_IOUT);
// r48_send_status_request(R48_BROADCAST_ADDRESS, R48_VALUETYPE_VOUT);
// r48_send_status_request(R48_BROADCAST_ADDRESS, R48_VALUETYPE_VIN);
r48_mem.last_status_broadcast_req_time = now;
#endif
}
// }
// }
}
static bool r48_send_u8_f32(r48_module_adr_t adr, uint8_t reg, float value)
{
if(adr >= R48_MAX_DEVICE_COUNT && adr != R48_BROADCAST_ADDRESS)
{
ESP_LOGE(TAG, "r48_send_u8_f32: module_adr 0x%02X out of range", adr);
return false;
}
twai_message_t msg = {0};
r48_can_id_t *can_id = (r48_can_id_t *)&msg.identifier;
can_id->bits.proto = R48_PROTO_MODULE_CONTROLLER_COMM;
can_id->bits.dst_addr = adr;
can_id->bits.src_addr = R48_THIS_DEVICE_ADDRESS;
can_id->bits.ptp = (adr != R48_BROADCAST_ADDRESS) ? 1 : 0;
// can_id->bits.cnt = 0;
can_id->bits.res1 = 1;
can_id->bits.res2 = 1;
msg.extd = 1; // Extended 29-bit
// msg.rtr = 0;
msg.data_length_code = 8;
r48_data_t *data = (r48_data_t *)msg.data;
// data->err = 0;
data->msgtype = R48_MSGTYPE_WRITE_DATA;
data->errType = R48_DATA_ERRTYPE_NONE;
// data->data.byte_wise.valuetype0_allways0 = 0x00;
data->data.byte_wise.valuetype1 = reg;
float_to_be(value, data->data.byte_wise.content);
return (twai_transmit(&msg, pdMS_TO_TICKS(100)) == ESP_OK);
}
static inline float amps_to_fraction(float amps)
{
if(amps < 0.f) amps = 0.f;
float frac = amps / R48_RATED_CURRENT_A; // 1.00 = 100% Nennstrom
// Viele Firmwares akzeptieren 0.10..1.21; 0.00 (abschalten) klappt nicht immer. :contentReference[oaicite:3]{index=3}
if(frac > 1.21f) frac = 1.21f;
return frac;
}
static inline float fraction_to_amps(float frac)
{
float amps = frac * R48_RATED_CURRENT_A;
return amps;
}
bool r48_set_current_offline(r48_module_adr_t module_adr, float amps)
{
float frac = amps_to_fraction(amps);
if(!r48_send_u8_f32(module_adr, R48_VALUETYPE_IOUT_OFFLINE, frac))
return false;
ESP_LOGI(TAG, "R48: Set offline current to %.2f A", amps);
//TODO: Check if this is needed when broadcasting or if we get feedback from the module
//As we parsing the current from the acknowledgement message, but if we broadcast, we possibly don't get a response
// if(module_adr != R48_BROADCAST_ADDRESS && r48_mem.devices[module_adr] != NULL)
// {
// r48_handle_t handle = r48_mem.devices[module_adr];
// handle->i_out_set = amps;
// handle->i_out_set_timestamp = esp_timer_get_time();
// }
return true;
}
bool r48_set_voltage_offline(r48_module_adr_t module_adr, float volts)
{
if(volts > R48_MAX_VOLTAGE_V)
{
ESP_LOGW(TAG, "R48: Requested voltage %.2f V exceeds maximum of %.2f V, clamping", volts, R48_MAX_VOLTAGE_V);
volts = R48_MAX_VOLTAGE_V;
}
ESP_LOGI(TAG, "R48: Set offline voltage to %.2f V", volts);
if(!r48_send_u8_f32(module_adr, R48_VALUETYPE_VOUT_OFFLINE, volts))
return false;
return true;
}
bool r48_set_current_online(r48_module_adr_t module_adr, float amps)
{
ESP_LOGI(TAG, "R48: Set online current to %.2f A", amps);
float frac = amps_to_fraction(amps);
if(!r48_send_u8_f32(module_adr, R48_VALUETYPE_IOUT_ONLINE, frac))
{
return false;
}
return true;
}
bool r48_set_voltage_online(r48_module_adr_t module_adr, float volts)
{
if(volts > R48_MAX_VOLTAGE_V)
{
ESP_LOGW(TAG, "R48: Requested voltage %.2f V exceeds maximum of %.2f V, clamping", volts, R48_MAX_VOLTAGE_V);
volts = R48_MAX_VOLTAGE_V;
}
ESP_LOGI(TAG, "R48: Set online voltage to %.2f V", volts);
if(!r48_send_u8_f32(module_adr, R48_VALUETYPE_VOUT_ONLINE, volts))
{
return false;
}
return true;
}
bool _r48_send_write_data(r48_module_adr_t module_adr, uint8_t valuetype, const uint8_t content[4])
{
twai_message_t msg = {0};
msg.extd = 1; // extended frame
msg.rtr = 0; // data frame
msg.data_length_code = 8;
r48_data_t *data = (r48_data_t *)msg.data;
r48_can_id_t *can_id = (r48_can_id_t *)&msg.identifier;
can_id->bits.proto = R48_PROTO_MODULE_CONTROLLER_COMM;
can_id->bits.dst_addr = module_adr;
can_id->bits.src_addr = R48_THIS_DEVICE_ADDRESS;
can_id->bits.ptp = (module_adr != R48_BROADCAST_ADDRESS) ? 1 : 0;
can_id->bits.cnt = 0;
can_id->bits.res1 = 1;
can_id->bits.res2 = 1;
data->msgtype = R48_MSGTYPE_WRITE_DATA;
// data->err = 0;
data->errType = R48_DATA_ERRTYPE_NONE;
// data->data.byte_wise.valuetype0_allways0 = 0x00;
data->data.byte_wise.valuetype1 = valuetype;
data->data.byte_wise.content[0] = content[0];
data->data.byte_wise.content[1] = content[1];
data->data.byte_wise.content[2] = content[2];
data->data.byte_wise.content[3] = content[3];
esp_err_t res = twai_transmit(&msg, pdMS_TO_TICKS(100));
if(res != ESP_OK)
{
ESP_LOGE(TAG, "Failed to send write data command: %s", esp_err_to_name(res));
return false;
}
return true;
}
bool _r48_remote_on_off(r48_module_adr_t module_adr, bool remote_on)
{
uint8_t content[4] = {0};
content[1] = remote_on ? 0x00 : 0x01; // 1 = remote off, 0 = remote on
if(!_r48_send_write_data(module_adr, R48_VALUETYPE_REMOTE_OFF, content))
{
ESP_LOGE(TAG, "Failed to send remote command: remote %s", remote_on ? "on" : "off");
return false;
}
if(module_adr != R48_BROADCAST_ADDRESS && r48_mem.devices[module_adr] != NULL)
{
r48_handle_t handle = r48_mem.devices[module_adr];
handle->remote_off = !remote_on;
if(remote_on)
{
//When enabling remote on, reset IOUT to last set value
handle->i_out = handle->i_out_set;
}
}
return true;
}
bool r48_remote_off(r48_module_adr_t module_adr)
{
return _r48_remote_on_off(module_adr, false);
}
bool r48_remote_on(r48_module_adr_t module_adr)
{
return _r48_remote_on_off(module_adr, true);
}
bool r48_fan_set_fullspeed(r48_module_adr_t module_adr)
{
uint8_t content[4] = {0x00, 0x01, 0x00, 0x00}; // 1 = full speed, 0 = auto
return _r48_send_write_data(module_adr, R48_VALUETYPE_FAN_CONTROL, content);
return true;
}
bool r48_fan_set_auto(r48_module_adr_t module_adr)
{
uint8_t content[4] = {0x00, 0x00, 0x00, 0x00}; // 1 = full speed, 0 = auto
return _r48_send_write_data(module_adr, R48_VALUETYPE_FAN_CONTROL, content);
}
bool r48_turn_on_off_module(bool on)
{
uint8_t content[4] = {0x00, on ? 0x00 : 0x01, 0x00, 0x00};
ESP_LOGI(TAG, "r48_turn_on_off_module: Turning module %s", on ? "ON" : "OFF");
return _r48_send_write_data(R48_BROADCAST_ADDRESS, R48_VALUETYPE_TURN_OFF_MODULE, content);
}
menuconfig HTV_R48_ENABLED
bool "Enable R48 Interface"
default n
help
Enable R48 CanBus interface.
if HTV_R48_ENABLED
config WEBMODULE_R48_3000E3_R48_HTML_INCLUDE
bool
default "n"
config HTV_R48_AS_WEBMODULE
bool "Enable R48 webserver module"
default y
depends on HTV_MODULE_WEBSERVER_ENABLED
select WEBMODULE_R48_3000E3_R48_HTML_INCLUDE
help
If you want to see R48 data on a webpage choice y here.
endif
Ok, I will start some programming work on it.A screen and knobs for adjusting sounds very good, much better than connecting to a computer to change settings.