• Hello ES! We could use some help to get us past the finish line on building the new knowledgebase for the forum.
    Can you donate? Please see our fundraising page. Thank you!

Emerson/Vertiv R48-series CAN programming?

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
 
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
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. 3657f6c8-65b6-420e-8a1c-ac0bb8dd464c.jpeg
 
Would really like to have the Code for this, because I don't have any experience in writing programs for an ESP32.
A friend programmed such a thing for controlling two Huawei R4875G5 connected in series, that gives me a nice 7kW charger for my 21S scooter battery.
Now I have a few Vertiv R48-4300e3 and would like to lighten my charger setup.
Problem is the 4300e3 needs to have sent the online voltage every 5 seconds or so, because they don't accept permanent values under 48V and I only need 87.5V.

It woud be perfect if the input current limit also could be changed with the ESP, that would allow the charger to run always at the upper limit of the different power sockets.
 
Last edited:
As promised here is some details of what i'm working on use at your own risk.

Hardware:
  • Seedstudio XIAO ESP32C6 - Link
  • CANBUS Board - Hailege 2pcs MCP2515 CAN Bus Module TJA1050 Receiver SPI Module for AVR Link
Notes:
  1. The reading back of values from the canbus are paused when sending permanent commands otherwise the permanent values never get set. Online values don't seem to need this pause and apply instantly.
  2. Reading back the current limit seems to take a little longer than other commands so theres a delay imposed after requesting current limit.
  3. Wifi can work in AP mode or Join your Home wifi, set
    1. WIFI_AP_MODE = false - connect to home wifi and set ssid and password in code - serial monitor will tell your the IP.
    2. WIFIF_AP_MODE - true - ESP32 acts as a Access point set ap_ssid and ap_password - not test this yet.
  4. Serial monitor in arduino editor logs a lot of data can be useful for debugging.
  5. Not fully tested all the functions yet. fan controls and walk don't seem to be working yet.
Libraries Required (Arduino Editor):
  1. mcp_can - by cory j fowler.
Screenshots:

screenshot.png

Code:

C++:
// 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;
      }
    }
  }
}
 
Last edited:
Tried your code today, but it does not finish compiling, it gets stuck and never ends.
Probably I have choosen the wrong librarys. There are dozens of them.
Probably you can give a hint, which are the right ones.
 
I'm presuming your using a different esp32 than the one i'm using?

As a test i copied the code from my post and made sure it compiles on my Arduino ide and it does, below i've included the libraries and the boards installed through board manager.

My guess would be you need to have the esp32 boards installed in arduino ide.

Libraries:

1755206738489.png

1755206772577.png


Boards Manager:
1755207152691.png


Board settings:

1755206880944.png
 
Now with the right libraries and removing the wrong ones , the compiling suceeded.
Just ordered the Xiao ESP32C6 , the MCP2515 and a 60V to 5V buck converter
Thanks a lot
 
Just got one of these, R48-3000e3, but came without an interface board to can't try it out.
Any suggestions for how to rig this up to test while I find and aquire a suitable board. I already have MCP2515 from an different can project, and was hoping to get straight to it...
 
Thanks for your input bois*

as you did help me i will share my code. You can find a lot of new information there.

Intresting is specially the format of the 29-bit Message idenfifier, the structure of the data, status bits and possible some new valuetypes...

For getting my code running you will have make some few changes as you will miss some libraries... It should support multiple devices on the same can bus.

It written for espressif 5.5.

C:
#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


C:
#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);
}

Code:
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
 
Last edited:
Hi all, I recently acquired an Emerson R48-3000e3 with a backplane interface board from ES member mcintyretj (great deal, thanks!)

I initially tried using martr84's code from post #105 with an esp32-c3 and bog standard can breakout board(I think same as he used)
and after figuring out some pin defs, I got the can module initialized, but I could not get the R48 to communicate/respond.

After spending some time researching, I think running the can module @3.3v was causing at least some of the issue.

So I pulled an Arduino nano(5v) out of the parts bin, and loaded rwyu90's code from post #59 , and success! can set the two voltages in their code,
43.5v and 50v.

So I am wondering how @martr84 got it working at 3.3v.

If I can't get the nice web interface working, I will probably write/borrow some come code for a simple screen with two knobs to adjust current/voltage and release some open source code.

Thoughts anyone?
 
Last edited:
A screen and knobs for adjusting sounds very good, much better than connecting to a computer to change settings.
 
Hello. When I found this thread, I was excited. I have (2) Vertiv R48-3500e3. After trying everything with an ESP32, I am told that some of these come locked down via firmware and cannot be unlocked. Therefore I cannot get more than the 53.5V. I am told the telltale sign is that the bus will not respond back.

Has anyone else found this to be true? I have tried offline and online commands and still get nothing. My board soldering skills are not very good, and why trying CAN commands.

In advance - thank you for any and all help
 
I have a firmware Update tool for Megmeet rectifiers and if you buy enough rectifiers they will make a firmware for you.
I have programmed more than 50 Vertiv rectifiers and had only one which was not responding to input current limit. Output voltage and output current was always working.

Are you shure you connected it the right way and have set the needed CAN speed and use the CAN channel where the rectifier is connected to?
 
Thank you for the response, Dominik. It makes me hopeful and would be great news if it is me and not the rectifier. I have (2) of these that I purchased to use as chargers for a solar system. I would like to even add more but the last few days have been discouraging.

I have attached photos of the rectifier label, backplane adapter, and SN65HVD230 adapter, that I am using.

For hardware connections, I am as follows:

- Adapter TX = IO4
- Adapter RX = IO5
- Adapter CANH = Right Backplane pin (AC power side)
- Adapter CANL = Left Backplane pin (DC power side)
- 120 ohm resistor across CANH and CANL at adapter
- 120 ohm resistor across CANH and CANL at backplane

- ESP32 S3 ground is also connected to DC-

The CAN speed is set to 125k

Originally, being naive, I thought to have code that can give me options (using Arduino IDE for compiling and uploading). When none would work, I tried simple OFFLINE (preferred) and ONLINE commands. Obviously, no luck.

**edited to correct typo for CANH and CANL cables
 

Attachments

  • SN65HVD230 .jpg
    SN65HVD230 .jpg
    64.4 KB · Views: 16
  • R48-3500_Backplane.Adapter.jpg
    R48-3500_Backplane.Adapter.jpg
    97.9 KB · Views: 16
  • _R48-3500e3.Label.jpg
    _R48-3500e3.Label.jpg
    106.1 KB · Views: 17
Last edited:
I never use the ground connection for my USB-CAN converter.
I have also never tried it with an ESP32.
 
Normally I use a Canalyst II.
Sometimes I use a CAN-Hacker, which is also based on a Microcontroller.
 
Dominik! You are a genius (at least in my eyes) !!

I spent 3 days trying everything that I could think of - as stated - NOTHING!! :(

In a last attempt before giving up, I took your posts and entered them into AI and asked, "Why does this work?" AI responded with the below (complete exchange). I have also copied the code to an additional standalone post for others to just grab if needed. In particular, look at the red text. LOL

AI SAYS........................

How this relates to your situation​

Key points for you:
  • His units respond on 0x0607FF83 with that 03 F0 00 XX command structure.
  • Your unit is only ever broadcasting 0x0707F803 with a fixed payload and never responding to anything.
  • Your CAN‑ID probe showed:
    • Every standard ID ACKed
    • No new IDs
    • No responses to any command pattern
So:
  • What he describes is the ideal, fully controllable behavior of R48‑2000E3/3500E3 when the firmware and wiring expose the real control bus.
  • What you’re seeing is not that—you’re not getting any of those responses or behaviors, which strongly suggests:
    • Different firmware variant, or
    • Different CAN wiring (internal gateway), or
    • Carrier‑locked build where the control path is not exposed on your pins.
If you want, next step we can do is: try exactly one of his frames (e.g. online voltage) on 0x0607FF83 and see if anything changes on your sniffer—ID, payload, or heartbeat. But given your probe results, I’d set expectations low: your module is behaving like the “supervised only, no remote control” variant.

try the 0x0607FF83

See my thinking

Good move—that’s the cleanest way to test whether your unit behaves like his or like the “supervised only” variant.
Here’s a minimal sketch that sends his exact online + offline voltage frames to 0x0607FF83 and lets you watch for any change on the bus.

🔧 Test: send his 52.5 V commands to 0x0607FF83​

cpp

#include <Arduino.h>
#include "driver/twai.h"

#define CAN_TX_PIN GPIO_NUM_4
#define CAN_RX_PIN GPIO_NUM_5

void setup() {
Serial.begin(115200);
delay(1500);

twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT(
CAN_TX_PIN, CAN_RX_PIN, TWAI_MODE_NORMAL
);
twai_timing_config_t t_config = TWAI_TIMING_CONFIG_125KBITS();
twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL();

if (twai_driver_install(&g_config, &t_config, &f_config) != ESP_OK) {
Serial.println("CAN driver install FAILED");
while (1);
}
if (twai_start() != ESP_OK) {
Serial.println("CAN start FAILED");
while (1);
}

Serial.println("=== TEST 0x0607FF83 VOLTAGE COMMANDS ===");

// Online voltage: 52.5 V (float 42 50 00 00)
twai_message_t online = {};
online.identifier = 0x0607FF83;
online.extd = 1; // EXTENDED ID
online.data_length_code = 8;
uint8_t online_data[8] = {0x03, 0xF0, 0x00, 0x21, 0x42, 0x50, 0x00, 0x00};
memcpy(online.data, online_data, 8);

// Offline voltage: 52.5 V (float 42 50 00 00)
twai_message_t offline = {};
offline.identifier = 0x0607FF83;
offline.extd = 1; // EXTENDED ID
offline.data_length_code = 8;
uint8_t offline_data[8] = {0x03, 0xF0, 0x00, 0x24, 0x42, 0x50, 0x00, 0x00};
memcpy(offline.data, offline_data, 8);

// Send both once at startup
if (twai_transmit(&online, pdMS_TO_TICKS(50)) == ESP_OK) {
Serial.println("Sent ONLINE voltage frame (52.5 V)");
} else {
Serial.println("Failed to send ONLINE frame");
}

delay(100);

if (twai_transmit(&offline, pdMS_TO_TICKS(50)) == ESP_OK) {
Serial.println("Sent OFFLINE voltage frame (52.5 V)");
} else {
Serial.println("Failed to send OFFLINE frame");
}
}

void loop() {
// Just sniff everything after sending the frames
twai_message_t msg;
if (twai_receive(&msg, pdMS_TO_TICKS(10)) == ESP_OK) {
uint32_t t = millis();
if (msg.extd) {
Serial.printf("[%10lu ms] ID: 0x%08X (EXT) DLC=%d DATA: ",
t, msg.identifier, msg.data_length_code);
} else {
Serial.printf("[%10lu ms] ID: 0x%03X (STD) DLC=%d DATA: ",
t, msg.identifier, msg.data_length_code);
}
for (int i = 0; i < msg.data_length_code; i++) {
Serial.printf("%02X ", msg.data);
}
Serial.println();
}
}

What to look for​

After flashing and running this:
  • Do you still only see 0x0707F803 with 11 D4 00 00 1E 2C 00 00?
  • Do you ever see:
    • A new ID (e.g. 0x0607FF83 echoed back)?
    • A change in the heartbeat payload?
    • Any additional frames right after the “Sent ONLINE/OFFLINE” messages?
If nothing at all changes—same single heartbeat, same data, no new IDs—then your unit is almost certainly not honoring that control path, even though it’s the same family.
Run this, paste 5–10 seconds of output after the “Sent … frame” lines, and we’ll call the verdict.

change the voltage to 55V so I can see a change.

Absolutely — here is the 55.0 V version of the exact same test sketch, with the float already converted and inserted correctly.

✔ IEEE‑754 float for 55.0 V

55.0 as a 32‑bit float =0x42 5C 00 00
(You can verify this with any float‑to‑hex tool.)

⚡ Updated CAN test sketch (55.0 V)

This sends:
  • Online voltage: 03 F0 00 21 42 5C 00 00
  • Offline voltage: 03 F0 00 24 42 5C 00 00
to 0x0607FF83 (EXT), exactly like the guy’s working units.
cpp

#include <Arduino.h>
#include "driver/twai.h"

#define CAN_TX_PIN GPIO_NUM_4
#define CAN_RX_PIN GPIO_NUM_5

void setup() {
Serial.begin(115200);
delay(1500);

twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT(
CAN_TX_PIN, CAN_RX_PIN, TWAI_MODE_NORMAL
);
twai_timing_config_t t_config = TWAI_TIMING_CONFIG_125KBITS();
twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL();

if (twai_driver_install(&g_config, &t_config, &f_config) != ESP_OK) {
Serial.println("CAN driver install FAILED");
while (1);
}
if (twai_start() != ESP_OK) {
Serial.println("CAN start FAILED");
while (1);
}

Serial.println("=== TEST 0x0607FF83 VOLTAGE COMMANDS (55.0V) ===");

// ONLINE voltage: 55.0 V (float 42 5C 00 00)
twai_message_t online = {};
online.identifier = 0x0607FF83;
online.extd = 1;
online.data_length_code = 8;
uint8_t online_data[8] = {0x03, 0xF0, 0x00, 0x21, 0x42, 0x5C, 0x00, 0x00};
memcpy(online.data, online_data, 8);

// OFFLINE voltage: 55.0 V (float 42 5C 00 00)
twai_message_t offline = {};
offline.identifier = 0x0607FF83;
offline.extd = 1;
offline.data_length_code = 8;
uint8_t offline_data[8] = {0x03, 0xF0, 0x00, 0x24, 0x42, 0x5C, 0x00, 0x00};
memcpy(offline.data, offline_data, 8);

// Send both once
if (twai_transmit(&online, pdMS_TO_TICKS(50)) == ESP_OK) {
Serial.println("Sent ONLINE voltage frame (55.0 V)");
} else {
Serial.println("Failed to send ONLINE frame");
}

delay(100);

if (twai_transmit(&offline, pdMS_TO_TICKS(50)) == ESP_OK) {
Serial.println("Sent OFFLINE voltage frame (55.0 V)");
} else {
Serial.println("Failed to send OFFLINE frame");
}
}

void loop() {
// Sniff everything after sending the frames
twai_message_t msg;
if (twai_receive(&msg, pdMS_TO_TICKS(10)) == ESP_OK) {
uint32_t t = millis();
if (msg.extd) {
Serial.printf("[%10lu ms] ID: 0x%08X (EXT) DLC=%d DATA: ",
t, msg.identifier, msg.data_length_code);
} else {
Serial.printf("[%10lu ms] ID: 0x%03X (STD) DLC=%d DATA: ",
t, msg.identifier, msg.data_length_code);
}
for (int i = 0; i < msg.data_length_code; i++) {
Serial.printf("%02X ", msg.data);
}
Serial.println();
}
}


⭐ What you should see if your rectifier is controllable​

If your unit behaves like the guy’s:
  • You will see new CAN IDs appear
  • Or the heartbeat frame will change
  • Or the rectifier will echo the command
  • Or the output voltage will jump to 55 V (if connected to a load)

❗ What you will see if your unit is locked / supervisory‑only​

  • No new IDs
  • No change in heartbeat
  • No echo
  • No telemetry
  • Still only 0x0707F803 every ~400 ms
This is exactly what your CAN‑ID probe suggested.

👉 Run this test and paste 5–10 seconds of sniffer output after the “Sent ONLINE/OFFLINE” messages.​

That will tell us instantly whether your rectifier is:
  • Fully controllable
  • Partially controllable
  • Or completely locked behind an internal supervisory MCU

After telling AI that IT WORKS! - I get.....

That’s huge — if the 55 V test worked, then your unit is fully controllable and not locked at all. You’ve just confirmed you’re on the real CAN control bus.
Let’s push it cleanly to 58.0 V, using the exact same command structure that worked for 55 V.

⚡ IEEE‑754 float for 58.0 V

58.0 as a 32‑bit float =0x42 68 00 00
(You’ve seen this before — it’s the same float you used earlier.)

⚡ Updated CAN commands for 58.0 V

✔ Online voltage (immediate change)​

Code

03 F0 00 21 42 68 00 00

✔ Offline voltage (permanent)​

Code

03 F0 00 24 42 68 00 00

Same CAN ID:

✔ CAN ID: 0x0607FF83 (EXTENDED)​

Same bitrate:

✔ 125 kbps​


⚡ Full working sketch (58.0 V)​

cpp

#include <Arduino.h>
#include "driver/twai.h"

#define CAN_TX_PIN GPIO_NUM_4
#define CAN_RX_PIN GPIO_NUM_5

void setup() {
Serial.begin(115200);
delay(1500);

twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT(
CAN_TX_PIN, CAN_RX_PIN, TWAI_MODE_NORMAL
);
twai_timing_config_t t_config = TWAI_TIMING_CONFIG_125KBITS();
twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL();

if (twai_driver_install(&g_config, &t_config, &f_config) != ESP_OK) {
Serial.println("CAN driver install FAILED");
while (1);
}
if (twai_start() != ESP_OK) {
Serial.println("CAN start FAILED");
while (1);
}

Serial.println("=== SETTING VOLTAGE TO 58.0V ===");

// ONLINE voltage: 58.0 V (float 42 68 00 00)
twai_message_t online = {};
online.identifier = 0x0607FF83;
online.extd = 1;
online.data_length_code = 8;
uint8_t online_data[8] = {0x03, 0xF0, 0x00, 0x21, 0x42, 0x68, 0x00, 0x00};
memcpy(online.data, online_data, 8);

// OFFLINE voltage: 58.0 V (float 42 68 00 00)
twai_message_t offline = {};
offline.identifier = 0x0607FF83;
offline.extd = 1;
offline.data_length_code = 8;
uint8_t offline_data[8] = {0x03, 0xF0, 0x00, 0x24, 0x42, 0x68, 0x00, 0x00};
memcpy(offline.data, offline_data, 8);

// Send both
if (twai_transmit(&online, pdMS_TO_TICKS(50)) == ESP_OK)
Serial.println("Sent ONLINE voltage frame (58.0 V)");
else
Serial.println("Failed to send ONLINE frame");

delay(100);

if (twai_transmit(&offline, pdMS_TO_TICKS(50)) == ESP_OK)
Serial.println("Sent OFFLINE voltage frame (58.0 V)");
else
Serial.println("Failed to send OFFLINE frame");
}

void loop() {
// Sniff everything after sending the frames
twai_message_t msg;
if (twai_receive(&msg, pdMS_TO_TICKS(10)) == ESP_OK) {
uint32_t t = millis();
if (msg.extd) {
Serial.printf("[%10lu ms] ID: 0x%08X (EXT) DLC=%d DATA: ",
t, msg.identifier, msg.data_length_code);
} else {
Serial.printf("[%10lu ms] ID: 0x%03X (STD) DLC=%d DATA: ",
t, msg.identifier, msg.data_length_code);
}
for (int i = 0; i < msg.data_length_code; i++)
Serial.printf("%02X ", msg.data);
Serial.println();
}
}


⭐ What you should see​

If your rectifier is behaving like the 55 V test:
  • Output voltage will rise to 58.0 V
  • The change will be immediate (online command)
  • It will stay at 58 V after reboot (offline command)
You’ve officially unlocked full control.
 
Clean post with code only, using ESP32 S3 Wroom 1, and SN65HVD230 adapter, with (2) 120 ohm resistors on each end of CANbus wires. Software was Arduino IDE 2.3.7, for compiling and uploading.

=================================================================
=================================================================




#include <Arduino.h>
#include "driver/twai.h"
#define CAN_TX_PIN GPIO_NUM_4
#define CAN_RX_PIN GPIO_NUM_5
void setup() {
Serial.begin(115200);
delay(1500);
twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT(
CAN_TX_PIN, CAN_RX_PIN, TWAI_MODE_NORMAL
);
twai_timing_config_t t_config = TWAI_TIMING_CONFIG_125KBITS();
twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL();
if (twai_driver_install(&g_config, &t_config, &f_config) != ESP_OK) {
Serial.println("CAN driver install FAILED");
while (1);
}
if (twai_start() != ESP_OK) {
Serial.println("CAN start FAILED");
while (1);
}
Serial.println("=== SETTING VOLTAGE TO 58.0V ===");
// ONLINE voltage: 58.0 V (float 42 68 00 00)
twai_message_t online = {};
online.identifier = 0x0607FF83;
online.extd = 1;
online.data_length_code = 8;
uint8_t online_data[8] = {0x03, 0xF0, 0x00, 0x21, 0x42, 0x68, 0x00, 0x00};
memcpy(online.data, online_data, 8);
// OFFLINE voltage: 58.0 V (float 42 68 00 00)
twai_message_t offline = {};
offline.identifier = 0x0607FF83;
offline.extd = 1;
offline.data_length_code = 8;
uint8_t offline_data[8] = {0x03, 0xF0, 0x00, 0x24, 0x42, 0x68, 0x00, 0x00};
memcpy(offline.data, offline_data, 8);
// Send both
if (twai_transmit(&online, pdMS_TO_TICKS(50)) == ESP_OK)
Serial.println("Sent ONLINE voltage frame (58.0 V)");
else
Serial.println("Failed to send ONLINE frame");
delay(100);
if (twai_transmit(&offline, pdMS_TO_TICKS(50)) == ESP_OK)
Serial.println("Sent OFFLINE voltage frame (58.0 V)");
else
Serial.println("Failed to send OFFLINE frame");
}
void loop() {
// Sniff everything after sending the frames
twai_message_t msg;
if (twai_receive(&msg, pdMS_TO_TICKS(10)) == ESP_OK) {
uint32_t t = millis();
if (msg.extd) {
Serial.printf("[%10lu ms] ID: 0x%08X (EXT) DLC=%d DATA: ",
t, msg.identifier, msg.data_length_code);
} else {
Serial.printf("[%10lu ms] ID: 0x%03X (STD) DLC=%d DATA: ",
t, msg.identifier, msg.data_length_code);
}
for (int i = 0; i < msg.data_length_code; i++)
Serial.printf("%02X ", msg.data);
Serial.println();
}
}
 
Greetings.
Got new R48-3000e3 and looks like it is in locked state. Green and yellow LED's are lit permanently.
I'm using WeAct USB2CANFD_V1 and cangaroo software.
I see packets from 0707F803 and when sending request for measurement values like temp, voltage etc., got a response data from 060F8003...
But faced to absolutely no reaction to any voltage/current output settings. It just outputs nothing.
Is there any way to turn it in controllable state?
Looks like cangaroo software is unable to send 2 different packets at once.
 

Attachments

  • 111.jpg
    111.jpg
    119.8 KB · Views: 17
0.523V
 

Attachments

  • IMG_20260221_113255.jpg
    IMG_20260221_113255.jpg
    278.6 KB · Views: 14
  • IMG_20260221_113306.jpg
    IMG_20260221_113306.jpg
    289.3 KB · Views: 18
Have you checked

DC current Control
DC-Output on 03 F0 00 30 00 00 00 00
DC-Output off 03 F0 00 30 00 01 00 00

and also have set input power limit to a reasonable value?
 
Back
Top