Make playback work if order is wrong

This commit is contained in:
Daniel Öster 2025-03-01 18:51:52 +02:00
parent e057df3345
commit 8c44da7070
3 changed files with 148 additions and 139 deletions

View file

@ -41,7 +41,7 @@
//#define TESLA_MODEL_SX_BATTERY //#define TESLA_MODEL_SX_BATTERY
//#define VOLVO_SPA_BATTERY //#define VOLVO_SPA_BATTERY
//#define VOLVO_SPA_HYBRID_BATTERY //#define VOLVO_SPA_HYBRID_BATTERY
#define TEST_FAKE_BATTERY //#define TEST_FAKE_BATTERY
//#define DOUBLE_BATTERY //Enable this line if you use two identical batteries at the same time (requires CAN_ADDON setup) //#define DOUBLE_BATTERY //Enable this line if you use two identical batteries at the same time (requires CAN_ADDON setup)
//#define SERIAL_LINK_TRANSMITTER //Enable this line to send battery data over RS485 pins to another Lilygo (This LilyGo interfaces with battery) //#define SERIAL_LINK_TRANSMITTER //Enable this line to send battery data over RS485 pins to another Lilygo (This LilyGo interfaces with battery)
@ -67,7 +67,7 @@
//#define SERIAL_LINK_RECEIVER //Enable this line to receive battery data over RS485 pins from another Lilygo (This LilyGo interfaces with inverter) //#define SERIAL_LINK_RECEIVER //Enable this line to receive battery data over RS485 pins from another Lilygo (This LilyGo interfaces with inverter)
/* Select hardware used for Battery-Emulator */ /* Select hardware used for Battery-Emulator */
#define HW_LILYGO //#define HW_LILYGO
//#define HW_STARK //#define HW_STARK
//#define HW_3LB //#define HW_3LB
//#define HW_DEVKIT //#define HW_DEVKIT
@ -81,7 +81,7 @@
//#define PERIODIC_BMS_RESET //Enable to have the emulator powercycle the connected battery every 24hours via GPIO. Useful for some batteries like Nissan LEAF //#define PERIODIC_BMS_RESET //Enable to have the emulator powercycle the connected battery every 24hours via GPIO. Useful for some batteries like Nissan LEAF
//#define REMOTE_BMS_RESET //Enable to allow the emulator to remotely trigger a powercycle of the battery via MQTT. Useful for some batteries like Nissan LEAF //#define REMOTE_BMS_RESET //Enable to allow the emulator to remotely trigger a powercycle of the battery via MQTT. Useful for some batteries like Nissan LEAF
// PERIODIC_BMS_RESET_AT Uses NTP server, internet required. In 24 Hour format WITHOUT leading 0. e.g 0230 should be 230. Time Zone is set in USER_SETTINGS.cpp // PERIODIC_BMS_RESET_AT Uses NTP server, internet required. In 24 Hour format WITHOUT leading 0. e.g 0230 should be 230. Time Zone is set in USER_SETTINGS.cpp
#define PERIODIC_BMS_RESET_AT 525 //#define PERIODIC_BMS_RESET_AT 525
/* Shunt/Contactor settings (Optional) */ /* Shunt/Contactor settings (Optional) */
//#define BMW_SBOX // SBOX relay control & battery current/voltage measurement //#define BMW_SBOX // SBOX relay control & battery current/voltage measurement
@ -101,7 +101,7 @@
//#define LOG_CAN_TO_SD //Enable this line to log incoming/outgoing CAN & CAN-FD messages to SD card (WARNING, raises CPU load, do not use for production) //#define LOG_CAN_TO_SD //Enable this line to log incoming/outgoing CAN & CAN-FD messages to SD card (WARNING, raises CPU load, do not use for production)
//#define DEBUG_VIA_USB //Enable this line to have the USB port output serial diagnostic data while program runs (WARNING, raises CPU load, do not use for production) //#define DEBUG_VIA_USB //Enable this line to have the USB port output serial diagnostic data while program runs (WARNING, raises CPU load, do not use for production)
//#define DEBUG_VIA_WEB //Enable this line to log diagnostic data while program runs, which can be viewed via webpage (WARNING, slightly raises CPU load, do not use for production) //#define DEBUG_VIA_WEB //Enable this line to log diagnostic data while program runs, which can be viewed via webpage (WARNING, slightly raises CPU load, do not use for production)
#define DEBUG_CAN_DATA //Enable this line to print incoming/outgoing CAN & CAN-FD messages to USB serial (WARNING, raises CPU load, do not use for production) #define DEBUG_CAN_DATA //Enable this line to print incoming/outgoing CAN & CAN-FD messages to USB serial (WARNING, raises CPU load, do not use for production)
/* CAN options */ /* CAN options */
//#define CAN_ADDON //Enable this line to activate an isolated secondary CAN Bus using add-on MCP2515 chip (Needed for some inverters / double battery) //#define CAN_ADDON //Enable this line to activate an isolated secondary CAN Bus using add-on MCP2515 chip (Needed for some inverters / double battery)

View file

@ -33,18 +33,16 @@ String can_replay_processor(void) {
// Dropdown with choices // Dropdown with choices
content += "<label for='canInterface'>CAN Interface:</label>"; content += "<label for='canInterface'>CAN Interface:</label>";
content += "<select id='canInterface' name='canInterface'>"; content += "<select id='canInterface' name='canInterface'>";
content += "<option value='" + String(CAN_NATIVE) + "' " + content += "<option value='" + String(CAN_NATIVE) + "' " +
(datalayer.system.info.can_replay_interface == CAN_NATIVE ? "selected" : "") + (datalayer.system.info.can_replay_interface == CAN_NATIVE ? "selected" : "") + ">CAN Native</option>";
">CAN Native</option>"; content += "<option value='" + String(CANFD_NATIVE) + "' " +
content += "<option value='" + String(CANFD_NATIVE) + "' " + (datalayer.system.info.can_replay_interface == CANFD_NATIVE ? "selected" : "") + ">CANFD Native</option>";
(datalayer.system.info.can_replay_interface == CANFD_NATIVE ? "selected" : "") + content += "<option value='" + String(CAN_ADDON_MCP2515) + "' " +
">CANFD Native</option>"; (datalayer.system.info.can_replay_interface == CAN_ADDON_MCP2515 ? "selected" : "") +
content += "<option value='" + String(CAN_ADDON_MCP2515) + "' " + ">CAN Addon MCP2515</option>";
(datalayer.system.info.can_replay_interface == CAN_ADDON_MCP2515 ? "selected" : "") + content += "<option value='" + String(CANFD_ADDON_MCP2518) + "' " +
">CAN Addon MCP2515</option>"; (datalayer.system.info.can_replay_interface == CANFD_ADDON_MCP2518 ? "selected" : "") +
content += "<option value='" + String(CANFD_ADDON_MCP2518) + "' " + ">CANFD Addon MCP2518</option>";
(datalayer.system.info.can_replay_interface == CANFD_ADDON_MCP2518 ? "selected" : "") +
">CANFD Addon MCP2518</option>";
content += "</select>"; content += "</select>";
@ -54,14 +52,14 @@ String can_replay_processor(void) {
content += "<h3>Step 2: Upload CAN Log File</h3>"; content += "<h3>Step 2: Upload CAN Log File</h3>";
content += "<div id='drop-area' onclick=\"document.getElementById('file-input').click()\">"; content += "<div id='drop-area' onclick=\"document.getElementById('file-input').click()\">";
content += "<p>Drag & drop a .txt file here, or click Browse to select one.</p>"; content += "<p>Drag & drop a .txt file here, or click Browse to select one.</p>";
content += "<input type='file' id='file-input' accept='.txt'>"; content += "<input type='file' id='file-input' accept='.txt'>";
content += "</div>"; content += "</div>";
content += "<div id='progress'><div id='progress-bar'></div></div>"; content += "<div id='progress'><div id='progress-bar'></div></div>";
content += "<button id='upload-btn'>Upload</button>"; content += "<button id='upload-btn'>Upload</button>";
content += "<h3>Step 3: Playback control</h3>"; content += "<h3>Step 3: Playback control</h3>";
@ -71,41 +69,50 @@ content += "<button id='upload-btn'>Upload</button>";
// Add a button to stop playing the log // Add a button to stop playing the log
content += "<button onclick='startReplay()'>Stop</button> "; content += "<button onclick='startReplay()'>Stop</button> ";
content += "<h3>Uploaded Log Preview:</h3>"; content += "<h3>Uploaded Log Preview:</h3>";
content += "<pre id='file-content'></pre>"; content += "<pre id='file-content'></pre>";
content += "<script>"; content += "<script>";
content += "const fileInput = document.getElementById('file-input');"; content += "const fileInput = document.getElementById('file-input');";
content += "const uploadBtn = document.getElementById('upload-btn');"; content += "const uploadBtn = document.getElementById('upload-btn');";
content += "const fileContent = document.getElementById('file-content');"; content += "const fileContent = document.getElementById('file-content');";
content += "const dropArea = document.getElementById('drop-area');"; content += "const dropArea = document.getElementById('drop-area');";
content += "const progressBar = document.getElementById('progress-bar');"; content += "const progressBar = document.getElementById('progress-bar');";
content += "const progressContainer = document.getElementById('progress');"; content += "const progressContainer = document.getElementById('progress');";
content += "let selectedFile = null;"; content += "let selectedFile = null;";
content += "dropArea.addEventListener('dragover', (e) => { e.preventDefault(); dropArea.style.background = '#f0f0f0'; });"; content +=
content += "dropArea.addEventListener('dragleave', () => { dropArea.style.background = 'white'; });"; "dropArea.addEventListener('dragover', (e) => { e.preventDefault(); dropArea.style.background = '#f0f0f0'; });";
content += "dropArea.addEventListener('drop', (e) => { e.preventDefault(); dropArea.style.background = 'white'; if (e.dataTransfer.files.length > 0) { fileInput.files = e.dataTransfer.files; selectedFile = fileInput.files[0]; }});"; content += "dropArea.addEventListener('dragleave', () => { dropArea.style.background = 'white'; });";
content +=
"dropArea.addEventListener('drop', (e) => { e.preventDefault(); dropArea.style.background = 'white'; if "
"(e.dataTransfer.files.length > 0) { fileInput.files = e.dataTransfer.files; selectedFile = fileInput.files[0]; "
"}});";
content += "fileInput.addEventListener('change', () => { selectedFile = fileInput.files[0]; });"; content += "fileInput.addEventListener('change', () => { selectedFile = fileInput.files[0]; });";
content += "uploadBtn.addEventListener('click', () => {"; content += "uploadBtn.addEventListener('click', () => {";
content += "if (!selectedFile) { alert('Please select a file first!'); return; }"; content += "if (!selectedFile) { alert('Please select a file first!'); return; }";
content += "const formData = new FormData();"; content += "const formData = new FormData();";
content += "formData.append('file', selectedFile);"; content += "formData.append('file', selectedFile);";
content += "const xhr = new XMLHttpRequest();"; content += "const xhr = new XMLHttpRequest();";
content += "xhr.open('POST', '/import_can_log', true);"; content += "xhr.open('POST', '/import_can_log', true);";
content += "xhr.upload.onprogress = (event) => { if (event.lengthComputable) { const percent = (event.loaded / event.total) * 100; progressContainer.style.display = 'block'; progressBar.style.width = percent + '%'; }};"; content +=
content += "xhr.onload = () => { if (xhr.status === 200) { alert('File uploaded successfully!'); progressBar.style.width = '100%'; const reader = new FileReader(); reader.onload = function (e) { fileContent.textContent = e.target.result; }; reader.readAsText(selectedFile); } else { alert('Upload failed! Server error.'); }};"; "xhr.upload.onprogress = (event) => { if (event.lengthComputable) { const percent = (event.loaded / event.total) "
content += "xhr.send(formData);"; "* 100; progressContainer.style.display = 'block'; progressBar.style.width = percent + '%'; }};";
content += "});"; content +=
content += "</script>"; "xhr.onload = () => { if (xhr.status === 200) { alert('File uploaded successfully!'); progressBar.style.width = "
"'100%'; const reader = new FileReader(); reader.onload = function (e) { fileContent.textContent = "
"e.target.result; }; reader.readAsText(selectedFile); } else { alert('Upload failed! Server error.'); }};";
content += "xhr.send(formData);";
content += "});";
content += "</script>";
content += "</div>"; content += "</div>";
// Add JavaScript for navigation // Add JavaScript for navigation
content += "<script>"; content += "<script>";
content += "function startReplay() {"; content += "function startReplay() {";
content += " var xhr = new XMLHttpRequest();"; content += " var xhr = new XMLHttpRequest();";
content += " xhr.open('GET', '/startReplay', true);"; content += " xhr.open('GET', '/startReplay', true);";
content += " xhr.send();"; content += " xhr.send();";

View file

@ -33,27 +33,25 @@ const char get_firmware_info_html[] = R"rawliteral(%X%)rawliteral";
String importedLogs = ""; // Store the uploaded file contents in RAM /WARNING THIS MIGHT GO BOOM String importedLogs = ""; // Store the uploaded file contents in RAM /WARNING THIS MIGHT GO BOOM
CAN_frame currentFrame = {.FD = false, CAN_frame currentFrame = {.FD = false,
.ext_ID = false, .ext_ID = false,
.DLC = 8, .DLC = 8,
.ID = 0x12F, .ID = 0x12F,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
void handleFileUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { void handleFileUpload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len,
if (!index) { bool final) {
importedLogs = ""; // Clear previous logs if (!index) {
Serial.printf("Receiving file: %s\n", filename.c_str()); importedLogs = ""; // Clear previous logs
} logging.printf("Receiving file: %s\n", filename.c_str());
}
// Append received data to the string (RAM storage) // Append received data to the string (RAM storage)
importedLogs += String((char*)data).substring(0, len); importedLogs += String((char*)data).substring(0, len);
if (final) { if (final) {
Serial.println("Upload Complete!"); logging.println("Upload Complete!");
Serial.println("Imported Log Data:"); request->send(200, "text/plain", "File uploaded successfully");
Serial.println(importedLogs); // Display contents for debugging (TODO: Remove these prints when feature works) }
//datalayer.system.info.logged_can_messages = importedLogs;
request->send(200, "text/plain", "File uploaded successfully");
}
} }
void init_webserver() { void init_webserver() {
@ -101,113 +99,117 @@ void init_webserver() {
request->send(response); request->send(response);
}); });
// Route for starting the CAN replay // Route for starting the CAN replay
server.on("/startReplay", HTTP_GET, [](AsyncWebServerRequest* request) { server.on("/startReplay", HTTP_GET, [](AsyncWebServerRequest* request) {
if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) { if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) {
return request->requestAuthentication(); return request->requestAuthentication();
} }
std::vector<String> messages; std::vector<String> messages;
int lastIndex = 0; int lastIndex = 0;
while (true) { while (true) {
int nextIndex = importedLogs.indexOf("\n", lastIndex); int nextIndex = importedLogs.indexOf("\n", lastIndex);
if (nextIndex == -1) { if (nextIndex == -1) {
messages.push_back(importedLogs.substring(lastIndex)); // Add last message messages.push_back(importedLogs.substring(lastIndex)); // Add last message
break; break;
} }
messages.push_back(importedLogs.substring(lastIndex, nextIndex)); messages.push_back(importedLogs.substring(lastIndex, nextIndex));
lastIndex = nextIndex + 1; lastIndex = nextIndex + 1;
} }
float firstTimestamp = -1.0; float firstTimestamp = -1.0;
float lastTimestamp = 0.0; float lastTimestamp = 0.0;
for (size_t i = 0; i < messages.size(); i++) { for (size_t i = 0; i < messages.size(); i++) {
String line = messages[i]; String line = messages[i];
line.trim(); // Remove leading/trailing spaces line.trim(); // Remove leading/trailing spaces
if (line.length() == 0) continue; // Skip empty lines if (line.length() == 0)
continue; // Skip empty lines
// Extract timestamp // Extract timestamp
int timeStart = line.indexOf("(") + 1; int timeStart = line.indexOf("(") + 1;
int timeEnd = line.indexOf(")"); int timeEnd = line.indexOf(")");
if (timeStart == 0 || timeEnd == -1) continue; if (timeStart == 0 || timeEnd == -1)
continue;
float currentTimestamp = line.substring(timeStart, timeEnd).toFloat(); float currentTimestamp = line.substring(timeStart, timeEnd).toFloat();
if (firstTimestamp < 0) { if (firstTimestamp < 0) {
firstTimestamp = currentTimestamp; // Store first message timestamp firstTimestamp = currentTimestamp; // Store first message timestamp
} }
// Calculate delay (skip for the first message) // Calculate delay (skip for the first message, and incase the log is out of order)
if (i > 0) { if ((i > 0) && (currentTimestamp > lastTimestamp)) {
float deltaT = (currentTimestamp - lastTimestamp) * 1000; // Convert seconds to milliseconds float deltaT = (currentTimestamp - lastTimestamp) * 1000; // Convert seconds to milliseconds
delay((int)deltaT); // Delay before sending this message
}
lastTimestamp = currentTimestamp; delay((int)deltaT); // Delay before sending this message
}
// Find the first space after the timestamp to locate the interface (TX# or RX#) lastTimestamp = currentTimestamp;
int interfaceStart = timeEnd + 2; // Start after ") "
int interfaceEnd = line.indexOf(" ", interfaceStart);
if (interfaceEnd == -1) continue;
String canInterface = line.substring(interfaceStart, interfaceEnd); // Extract TX# or RX# // Find the first space after the timestamp to locate the interface (TX# or RX#)
int interfaceStart = timeEnd + 2; // Start after ") "
int interfaceEnd = line.indexOf(" ", interfaceStart);
if (interfaceEnd == -1)
continue;
// Extract CAN ID String canInterface = line.substring(interfaceStart, interfaceEnd); // Extract TX# or RX#
int idStart = interfaceEnd + 1;
int idEnd = line.indexOf(" [", idStart);
if (idStart == -1 || idEnd == -1) continue;
String messageID = line.substring(idStart, idEnd); // Extract CAN ID
int idStart = interfaceEnd + 1;
int idEnd = line.indexOf(" [", idStart);
if (idStart == -1 || idEnd == -1)
continue;
// Extract DLC String messageID = line.substring(idStart, idEnd);
int dlcStart = idEnd + 2;
int dlcEnd = line.indexOf("]", dlcStart);
if (dlcEnd == -1) continue;
String dlc = line.substring(dlcStart, dlcEnd); // Extract DLC
int dlcStart = idEnd + 2;
int dlcEnd = line.indexOf("]", dlcStart);
if (dlcEnd == -1)
continue;
// Extract data bytes String dlc = line.substring(dlcStart, dlcEnd);
int dataStart = dlcEnd + 2;
String dataBytes = line.substring(dataStart);
// Assign values to the CAN frame // Extract data bytes
currentFrame.ID = strtol(messageID.c_str(), NULL, 16); int dataStart = dlcEnd + 2;
currentFrame.DLC = dlc.toInt(); String dataBytes = line.substring(dataStart);
// Parse and store data bytes // Assign values to the CAN frame
int byteIndex = 0; currentFrame.ID = strtol(messageID.c_str(), NULL, 16);
char* token = strtok((char*)dataBytes.c_str(), " "); currentFrame.DLC = dlc.toInt();
while (token != NULL && byteIndex < 8) {
currentFrame.data.u8[byteIndex++] = strtol(token, NULL, 16);
token = strtok(NULL, " ");
}
// Transmit the CAN frame // Parse and store data bytes
transmit_can_frame(&currentFrame, datalayer.system.info.can_replay_interface); int byteIndex = 0;
char* token = strtok((char*)dataBytes.c_str(), " ");
while (token != NULL && byteIndex < 8) {
currentFrame.data.u8[byteIndex++] = strtol(token, NULL, 16);
token = strtok(NULL, " ");
}
// Transmit the CAN frame
transmit_can_frame(&currentFrame, datalayer.system.info.can_replay_interface);
} }
request->send(200, "text/plain", "All CAN messages sent with correct interfaces!"); request->send(200, "text/plain", "All CAN messages sent with correct interfaces!");
}); });
// Route to handle setting the CAN interface for CAN replay // Route to handle setting the CAN interface for CAN replay
server.on("/setCANInterface", HTTP_GET, [](AsyncWebServerRequest* request) { server.on("/setCANInterface", HTTP_GET, [](AsyncWebServerRequest* request) {
if (request->hasParam("interface")) { if (request->hasParam("interface")) {
String canInterface = request->getParam("interface")->value(); String canInterface = request->getParam("interface")->value();
// Convert the received value to an integer
int interfaceValue = canInterface.toInt();
// Update the datalayer with the selected interface // Convert the received value to an integer
datalayer.system.info.can_replay_interface = interfaceValue; int interfaceValue = canInterface.toInt();
// Respond with success message // Update the datalayer with the selected interface
request->send(200, "text/plain", "CAN Interface Updated to " + canInterface); datalayer.system.info.can_replay_interface = interfaceValue;
// Respond with success message
request->send(200, "text/plain", "CAN Interface Updated to " + canInterface);
} else { } else {
request->send(400, "text/plain", "Error: Missing parameter 'interface'"); request->send(400, "text/plain", "Error: Missing parameter 'interface'");
} }
}); });
@ -226,12 +228,12 @@ server.on("/startReplay", HTTP_GET, [](AsyncWebServerRequest* request) {
}); });
// Define the handler to import can log // Define the handler to import can log
server.on("/import_can_log", HTTP_POST,[](AsyncWebServerRequest *request) { server.on(
"/import_can_log", HTTP_POST,
[](AsyncWebServerRequest* request) {
request->send(200, "text/plain", "Ready to receive file."); // Response when request is made request->send(200, "text/plain", "Ready to receive file."); // Response when request is made
}, },
handleFileUpload handleFileUpload);
);
#ifndef LOG_CAN_TO_SD #ifndef LOG_CAN_TO_SD
// Define the handler to export can log // Define the handler to export can log