/*******************************************************
* 4-CYLINDER ESP32 ECU SYSTEM
*
* Professional Features:
* - 4 injectors and 4 ignition channels
* - Dynamic fuel AND ignition maps with customizable RPM range
* - Professional web-based tuning interface with dual map editors
* - Complete lighting controls (headlights, turns, brakes)
* - Real-time sensor monitoring and fail-safes
* - Advanced fuel & ignition map editors with 0-RPM to limit range
* - Multiple fuel & ignition map support with selection interface
* - WORKING 3D visualization and graph views for both maps
*******************************************************/
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <ArduinoJson.h>
#include <EEPROM.h>
// ==================== CONFIGURATION ====================
const char* ssid = "ESP32_ECU";
const char* password = "tune12345";
// ==================== PIN DEFINITIONS ====================
// Fuel Injectors
#define PIN_INJECTOR_1 12
#define PIN_INJECTOR_2 14
#define PIN_INJECTOR_3 27
#define PIN_INJECTOR_4 26
// Ignition Coils
#define PIN_IGNITION_1 9
#define PIN_IGNITION_2 13
#define PIN_IGNITION_3 11
#define PIN_IGNITION_4 10
// Lighting Outputs
#define PIN_HEADLIGHT 17
#define PIN_HIGHBEAM 16
#define PIN_LEFT_TURN 5
#define PIN_RIGHT_TURN 4
#define PIN_BRAKE_LIGHT 8
// Sensor Inputs
#define PIN_O2_SENSOR 32
#define PIN_TPS 33
#define PIN_MAP_SENSOR 34
#define PIN_ENGINE_TEMP 35
#define PIN_RPM 17
// ==================== GLOBAL VARIABLES ====================
AsyncWebServer server(80);
// Engine State
volatile unsigned long rpmPulseTime = 0;
volatile unsigned long lastRpmPulse = 0;
float currentRPM = 0;
float o2Value = 14.7;
float tpsValue = 0;
float mapValue = 0;
float engineTemp = 20;
float batteryVoltage = 12.5;
// System State
bool systemEnabled = false;
bool fuelPumpRunning = false;
bool criticalFault = false;
String faultMessage = "";
// Injection Tracking
unsigned long lastInjectorPulse[4] = {0, 0, 0, 0};
int currentPulseWidth[4] = {0, 0, 0, 0};
unsigned long engineRunTime = 0;
// Lighting State
bool headlightState = false;
bool highbeamState = false;
bool leftTurnState = false;
bool rightTurnState = false;
bool brakeLightState = false;
bool hazardState = false;
unsigned long turnSignalPreviousMillis = 0;
bool turnSignalBlinkState = false;
const long turnSignalInterval = 500;
// Tuning Parameters
struct TuningParams {
float fuelMultiplier = 1.0;
float ignitionAdvance = 12.0;
float targetAFR = 14.7;
int rpmLimit = 8000;
int tempLimit = 105;
int minRPM = 0;
int maxRPM = 8000;
int rpmStep = 250;
int loadStep = 10;
String currentTune = "performance";
String fuelType = "gasoline";
};
TuningParams tuning;
// Advanced Fuel & Ignition Map Structures
#define MAX_RPM_POINTS 100 // Support up to 100 RPM points (0-10000 RPM)
#define MAX_LOAD_POINTS 11 // 0-100% in 10% steps + 0%
struct FuelMapCell {
int pulseWidth;
bool learned;
float correction;
};
struct IgnitionMapCell {
float advance;
bool learned;
float correction;
};
struct FuelMap {
String name;
String description;
String fuelType;
int rpmPoints[MAX_RPM_POINTS];
int loadPoints[MAX_LOAD_POINTS];
FuelMapCell cells[MAX_RPM_POINTS][MAX_LOAD_POINTS];
int rpmPointCount;
int loadPointCount;
};
struct IgnitionMap {
String name;
String description;
int rpmPoints[MAX_RPM_POINTS];
int loadPoints[MAX_LOAD_POINTS];
IgnitionMapCell cells[MAX_RPM_POINTS][MAX_LOAD_POINTS];
int rpmPointCount;
int loadPointCount;
};
FuelMap fuelMap;
IgnitionMap ignitionMap;
bool isFlashing = false;
unsigned long flashStartTime = 0;
// Multiple fuel & ignition maps storage
#define MAX_FUEL_MAPS 10
#define MAX_IGNITION_MAPS 10
FuelMap fuelMaps[MAX_FUEL_MAPS];
IgnitionMap ignitionMaps[MAX_IGNITION_MAPS];
int currentFuelMapIndex = 0;
int currentIgnitionMapIndex = 0;
int fuelMapCount = 0;
int ignitionMapCount = 0;
// Firing order (1-3-4-2 typical for inline-4)
const int firingOrder[4] = {0, 2, 3, 1}; // Injector order
const int ignitionOrder[4] = {0, 2, 3, 1}; // Ignition order
int currentCylinder = 0;
unsigned long lastCylinderTime = 0;
// ==================== EMBEDDED WEBPAGE ====================
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html>
<head>
<title>Professional 4-Cylinder ECU Tuner</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chart.js/3.7.0/chart.min.js"></script>
<style>
:root {
--primary: #2c3e50;
--secondary: #3498db;
--success: #27ae60;
--danger: #e74c3c;
--warning: #f39c12;
--info: #17a2b8;
--dark: #34495e;
--light: #ecf0f1;
--fuel-color: #e74c3c;
--ignition-color: #3498db;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #333;
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1600px;
margin: 0 auto;
}
.header {
background: white;
padding: 20px;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
margin-bottom: 20px;
text-align: center;
}
.header h1 {
color: var(--primary);
margin-bottom: 10px;
}
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
@media (max-width: 768px) {
.grid {
grid-template-columns: 1fr;
}
}
.card {
background: white;
padding: 25px;
border-radius: 15px;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.card h2 {
color: var(--primary);
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid var(--secondary);
}
/* Map Editor Tabs */
.map-tabs {
display: flex;
background: var(--light);
border-radius: 8px;
padding: 5px;
margin-bottom: 20px;
}
.map-tab {
padding: 12px 20px;
border: none;
background: transparent;
border-radius: 5px;
cursor: pointer;
font-weight: 600;
transition: all 0.3s ease;
flex: 1;
text-align: center;
}
.map-tab.active {
background: var(--secondary);
color: white;
}
.map-tab.fuel {
border-bottom: 3px solid var(--fuel-color);
}
.map-tab.ignition {
border-bottom: 3px solid var(--ignition-color);
}
/* Sensor Grid */
.sensor-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
}
.sensor-item {
text-align: center;
padding: 15px;
background: var(--light);
border-radius: 10px;
}
.sensor-value {
font-size: 1.8em;
font-weight: bold;
color: var(--primary);
margin: 5px 0;
}
.sensor-unit {
color: #7f8c8d;
font-size: 0.9em;
}
.gauge {
width: 100%;
height: 20px;
background: var(--light);
border-radius: 10px;
overflow: hidden;
margin: 10px 0;
}
.gauge-fill {
height: 100%;
background: linear-gradient(90deg, var(--success), var(--warning), var(--danger));
transition: width 0.5s ease;
}
/* Buttons and Controls */
.btn {
padding: 12px 20px;
margin: 5px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: all 0.3s ease;
min-width: 110px;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
.btn-primary { background: var(--secondary); color: white; }
.btn-success { background: var(--success); color: white; }
.btn-danger { background: var(--danger); color: white; }
.btn-warning { background: var(--warning); color: white; }
.btn-info { background: var(--info); color: white; }
.btn-fuel { background: var(--fuel-color); color: white; }
.btn-ignition { background: var(--ignition-color); color: white; }
.btn-active {
background: var(--dark) !important;
color: white;
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.3);
}
.control-group {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 10px;
margin: 15px 0;
}
.preset-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 10px;
margin: 15px 0;
}
/* Map Table */
.map-container {
overflow-x: auto;
margin: 20px 0;
}
.map-table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
background: white;
}
.map-table th,
.map-table td {
padding: 8px;
text-align: center;
border: 1px solid #ddd;
min-width: 60px;
}
.map-table th {
background: var(--primary);
color: white;
position: sticky;
top: 0;
}
.map-table th:first-child {
position: sticky;
left: 0;
background: var(--dark);
z-index: 2;
}
.map-table td:first-child {
position: sticky;
left: 0;
background: var(--light);
font-weight: bold;
z-index: 1;
}
.map-table input {
width: 100%;
padding: 4px;
border: 1px solid #ddd;
border-radius: 3px;
text-align: center;
font-size: 11px;
}
.map-table input:focus {
border-color: var(--secondary);
outline: none;
background: #f0f8ff;
}
/* Lighting Controls */
.lighting-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin: 15px 0;
}
.light-item {
background: var(--light);
padding: 15px;
border-radius: 10px;
text-align: center;
}
.light-status {
font-size: 1.2em;
font-weight: bold;
margin: 10px 0;
}
.status-on { color: var(--success); }
.status-off { color: var(--danger); }
.status-blinking { color: var(--warning); animation: blink 1s infinite; }
@keyframes blink { 50% { opacity: 0.5; } }
/* Tuning Controls */
.slider-container {
margin: 15px 0;
}
.slider-container label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: var(--primary);
}
.slider-value {
float: right;
font-weight: bold;
color: var(--secondary);
}
input[type="range"] {
width: 100%;
height: 8px;
border-radius: 4px;
background: var(--light);
outline: none;
}
.status-indicator {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 8px;
}
.status-on { background: var(--success); }
.status-off { background: var(--danger); }
.status-fault { background: var(--warning); animation: pulse 1s infinite; }
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
.fault-panel {
background: var(--danger);
color: white;
padding: 15px;
border-radius: 10px;
margin: 10px 0;
text-align: center;
display: none;
}
.map-controls {
display: flex;
justify-content: space-between;
align-items: center;
margin: 15px 0;
flex-wrap: wrap;
gap: 10px;
}
/* Cylinder Info */
.cylinder-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
margin: 15px 0;
}
.cylinder-item {
background: var(--light);
padding: 15px;
border-radius: 10px;
text-align: center;
}
.cylinder-active {
background: #e8f4fd;
border: 2px solid var(--secondary);
}
/* Tune Info */
.tune-info {
background: #e8f4fd;
padding: 15px;
border-radius: 10px;
margin: 10px 0;
border-left: 4px solid var(--secondary);
}
/* Map Selection */
.map-selection-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin: 15px 0;
}
.map-item {
background: var(--light);
padding: 15px;
border-radius: 10px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
border: 2px solid transparent;
}
.map-item:hover {
transform: translateY(-3px);
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.map-item.active {
border-color: var(--secondary);
background: #e8f4fd;
}
.map-item h3 {
color: var(--primary);
margin-bottom: 10px;
}
.map-meta {
font-size: 0.8em;
color: #7f8c8d;
margin: 5px 0;
}
.map-actions {
margin-top: 10px;
display: flex;
gap: 5px;
justify-content: center;
}
.map-actions .btn {
padding: 6px 12px;
font-size: 12px;
min-width: auto;
}
/* View Toggle */
.view-toggle {
display: flex;
background: var(--light);
border-radius: 8px;
padding: 5px;
}
.view-toggle button {
padding: 8px 15px;
border: none;
background: transparent;
border-radius: 5px;
cursor: pointer;
}
.view-toggle button.active {
background: var(--secondary);
color: white;
}
/* 3D View */
.map-3d {
width: 100%;
height: 400px;
background: #1a1a1a;
border-radius: 10px;
margin: 15px 0;
position: relative;
overflow: hidden;
}
.map-3d canvas {
border-radius: 10px;
}
/* Graph View */
.map-graph {
width: 100%;
height: 400px;
background: white;
border-radius: 10px;
margin: 15px 0;
position: relative;
}
.map-graph canvas {
border-radius: 10px;
}
/* Map Editor Sections */
.map-editor-section {
display: none;
}
.map-editor-section.active {
display: block;
}
/* Visualization Controls */
.viz-controls {
position: absolute;
top: 10px;
right: 10px;
background: rgba(0,0,0,0.7);
padding: 10px;
border-radius: 5px;
z-index: 10;
}
.viz-controls button {
background: #444;
color: white;
border: none;
padding: 5px 10px;
margin: 2px;
border-radius: 3px;
cursor: pointer;
}
.viz-controls button:hover {
background: #666;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Professional 4-Cylinder ECU Tuner</h1>
<p>Advanced Engine Management & Lighting Control</p>
</div>
<div class="fault-panel" id="faultPanel">
<strong>CRITICAL FAULT:</strong> <span id="faultMessage"></span>
</div>
<div class="grid">
<!-- Sensor Data Panel -->
<div class="card">
<h2>Live Engine Data</h2>
<div class="sensor-grid">
<div class="sensor-item">
<div>Engine RPM</div>
<div class="sensor-value" id="rpmValue">0</div>
<div class="sensor-unit">RPM</div>
<div class="gauge"><div class="gauge-fill" id="rpmGauge" style="width: 0%"></div></div>
</div>
<div class="sensor-item">
<div>Air/Fuel Ratio</div>
<div class="sensor-value" id="o2Value">0.00</div>
<div class="sensor-unit">AFR</div>
<div class="gauge"><div class="gauge-fill" id="o2Gauge" style="width: 0%"></div></div>
</div>
<div class="sensor-item">
<div>Throttle Position</div>
<div class="sensor-value" id="tpsValue">0%</div>
<div class="sensor-unit">Percentage</div>
<div class="gauge"><div class="gauge-fill" id="tpsGauge" style="width: 0%"></div></div>
</div>
<div class="sensor-item">
<div>Engine Load</div>
<div class="sensor-value" id="mapValue">0%</div>
<div class="sensor-unit">MAP</div>
<div class="gauge"><div class="gauge-fill" id="mapGauge" style="width: 0%"></div></div>
</div>
<div class="sensor-item">
<div>Engine Temp</div>
<div class="sensor-value" id="tempValue">0°C</div>
<div class="sensor-unit">Celsius</div>
<div class="gauge"><div class="gauge-fill" id="tempGauge" style="width: 0%"></div></div>
</div>
<div class="sensor-item">
<div>Battery Voltage</div>
<div class="sensor-value" id="voltageValue">0.0</div>
<div class="sensor-unit">Volts</div>
<div class="gauge"><div class="gauge-fill" id="voltageGauge" style="width: 0%"></div></div>
</div>
</div>
<div style="margin-top: 20px; padding: 15px; background: #f8f9fa; border-radius: 10px;">
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap;">
<div>
<span class="status-indicator" id="systemStatusLight"></span>
System: <span id="systemStatusText">OFF</span>
</div>
<div>Active Cylinder: <span id="activeCylinder">-</span></div>
<div>Fuel: <span id="fuelType">gasoline</span></div>
<div>Tune: <span id="currentTune">performance</span></div>
</div>
</div>
<!-- Cylinder Information -->
<h3>Cylinder Status</h3>
<div class="cylinder-grid">
<div class="cylinder-item" id="cyl1">
<strong>Cylinder 1</strong><br>
Pulse: <span id="pulse1">0</span>µs<br>
Timing: <span id="timing1">0</span>°
</div>
<div class="cylinder-item" id="cyl2">
<strong>Cylinder 2</strong><br>
Pulse: <span id="pulse2">0</span>µs<br>
Timing: <span id="timing2">0</span>°
</div>
<div class="cylinder-item" id="cyl3">
<strong>Cylinder 3</strong><br>
Pulse: <span id="pulse3">0</span>µs<br>
Timing: <span id="timing3">0</span>°
</div>
<div class="cylinder-item" id="cyl4">
<strong>Cylinder 4</strong><br>
Pulse: <span id="pulse4">0</span>µs<br>
Timing: <span id="timing4">0</span>°
</div>
</div>
</div>
<!-- System Control Panel -->
<div class="card">
<h2>System Controls</h2>
<div class="control-group">
<button class="btn btn-success" onclick="toggleSystem()">
<span class="status-indicator status-off" id="powerLight"></span>
Power: <span id="powerStatus">OFF</span>
</button>
<button class="btn btn-primary" onclick="toggleFuelPump()">
Fuel Pump: <span id="pumpStatus">OFF</span>
</button>
</div>
<div class="tune-info">
<strong>Active Tune:</strong> <span id="activeTuneName">Performance</span><br>
<span id="tuneDescription">Balanced performance for street use</span>
</div>
<h3>Preset Tunes</h3>
<div class="preset-grid">
<button class="btn btn-info" onclick="applyPreset('economy')">Economy</button>
<button class="btn btn-success" onclick="applyPreset('performance')">Performance</button>
<button class="btn btn-warning" onclick="applyPreset('race')">Race</button>
<button class="btn btn-primary" onclick="applyPreset('methanol')">Methanol</button>
<button class="btn btn-primary" onclick="applyPreset('gasoline')">Gasoline</button>
</div>
<h3>Lighting Controls</h3>
<div class="lighting-grid">
<div class="light-item">
<div>Headlights</div>
<div class="light-status status-off" id="headlightStatus">OFF</div>
<button class="btn btn-primary" onclick="toggleHeadlights()">Toggle</button>
</div>
<div class="light-item">
<div>High Beams</div>
<div class="light-status status-off" id="highbeamStatus">OFF</div>
<button class="btn btn-primary" onclick="toggleHighbeams()">Toggle</button>
</div>
<div class="light-item">
<div>Left Turn</div>
<div class="light-status status-off" id="leftTurnStatus">OFF</div>
<button class="btn btn-warning" onclick="toggleLeftTurn()">Toggle</button>
</div>
<div class="light-item">
<div>Right Turn</div>
<div class="light-status status-off" id="rightTurnStatus">OFF</div>
<button class="btn btn-warning" onclick="toggleRightTurn()">Toggle</button>
</div>
<div class="light-item">
<div>Brake Lights</div>
<div class="light-status status-off" id="brakeLightStatus">OFF</div>
<button class="btn btn-danger" onclick="toggleBrakeLights()">Toggle</button>
</div>
<div class="light-item">
<div>Hazard Lights</div>
<div class="light-status status-off" id="hazardStatus">OFF</div>
<button class="btn btn-warning" onclick="toggleHazards()">Toggle</button>
</div>
</div>
<h3>Quick Tuning</h3>
<div class="slider-container">
<label>Fuel Multiplier: <span class="slider-value" id="fuelMultiplierValue">1.00</span></label>
<input type="range" id="fuelMultiplier" min="0.5" max="1.8" step="0.01" value="1.0" oninput="updateFuelMultiplier(this.value)">
</div>
<div class="slider-container">
<label>Ignition Advance: <span class="slider-value" id="ignitionAdvanceValue">12.0</span></label>
<input type="range" id="ignitionAdvance" min="5.0" max="35.0" step="0.5" value="12.0" oninput="updateIgnitionAdvance(this.value)">
</div>
<div class="slider-container">
<label>Target AFR: <span class="slider-value" id="targetAFRValue">14.7</span></label>
<input type="range" id="targetAFR" min="6.0" max="18.0" step="0.1" value="14.7" oninput="updateTargetAFR(this.value)">
</div>
<div class="slider-container">
<label>RPM Limit: <span class="slider-value" id="rpmLimitValue">8000</span></label>
<input type="range" id="rpmLimit" min="3000" max="12000" step="100" value="8000" oninput="updateRPMLimit(this.value)">
</div>
<div class="control-group">
<button class="btn btn-primary" onclick="saveTuning()">Save Tuning</button>
<button class="btn btn-primary" onclick="loadTuning()">Load Tuning</button>
<button class="btn btn-danger" onclick="resetTuning()">Reset Defaults</button>
</div>
</div>
</div>
<!-- Map Selection Panel -->
<div class="card">
<h2>Map Selection</h2>
<div class="map-tabs">
<button class="map-tab fuel active" onclick="switchMapType('fuel')">Fuel Maps</button>
<button class="map-tab ignition" onclick="switchMapType('ignition')">Ignition Maps</button>
</div>
<!-- Fuel Map Selection -->
<div id="fuelMapSelection" class="map-selection-section">
<div style="margin-bottom: 15px;">
<strong>Active Fuel Map:</strong> <span id="selectedFuelMap">Base Fuel Map</span> |
<strong>Status:</strong> <span id="fuelMapStatus">Loaded</span>
</div>
<div class="map-selection-grid" id="fuelMapSelectionGrid">
<!-- Fuel maps will be populated by JavaScript -->
</div>
</div>
<!-- Ignition Map Selection -->
<div id="ignitionMapSelection" class="map-selection-section" style="display: none;">
<div style="margin-bottom: 15px;">
<strong>Active Ignition Map:</strong> <span id="selectedIgnitionMap">Base Ignition Map</span> |
<strong>Status:</strong> <span id="ignitionMapStatus">Loaded</span>
</div>
<div class="map-selection-grid" id="ignitionMapSelectionGrid">
<!-- Ignition maps will be populated by JavaScript -->
</div>
</div>
<div class="control-group" style="margin-top: 15px;">
<button class="btn btn-primary" onclick="showUploadDialog()">Upload Custom Map</button>
<button class="btn btn-info" onclick="createNewMap()">Create New Map</button>
<button class="btn btn-warning" onclick="flashToECU()">Flash Selected to ECU</button>
</div>
</div>
<!-- Professional Map Editor -->
<div class="card">
<h2>Professional Map Editor</h2>
<div class="map-tabs">
<button class="map-tab fuel active" onclick="switchEditor('fuel')">Fuel Map Editor</button>
<button class="map-tab ignition" onclick="switchEditor('ignition')">Ignition Map Editor</button>
</div>
<!-- Fuel Map Editor -->
<div id="fuelMapEditor" class="map-editor-section active">
<div class="map-controls">
<div>
<strong>Editing:</strong> <span id="editingFuelMapName">Base Fuel Map</span>
</div>
<div class="view-toggle">
<button class="active" onclick="switchFuelView('table')">Table View</button>
<button onclick="switchFuelView('3d')">3D View</button>
<button onclick="switchFuelView('graph')">Graph View</button>
</div>
<div>
<button class="btn btn-fuel" onclick="saveCurrentFuelMap()">Save Fuel Map</button>
<button class="btn btn-info" onclick="exportFuelMap()">Export Fuel Map</button>
</div>
</div>
<!-- Table View -->
<div id="fuelTableView">
<div class="map-container">
<table class="map-table" id="fuelMapTable">
<thead id="fuelMapHeader">
<!-- Header will be populated by JavaScript -->
</thead>
<tbody id="fuelMapBody">
<!-- Fuel map will be populated by JavaScript -->
</tbody>
</table>
</div>
</div>
<!-- 3D View -->
<div id="fuelView3d" style="display: none;">
<div class="map-3d" id="fuel3dContainer">
<div class="viz-controls">
<button onclick="resetFuel3DView()">Reset View</button>
<button onclick="toggleFuelWireframe()">Wireframe</button>
</div>
</div>
</div>
<!-- Graph View -->
<div id="fuelViewGraph" style="display: none;">
<div class="map-graph">
<canvas id="fuelGraphCanvas"></canvas>
</div>
</div>
<div class="control-group" style="margin-top: 15px;">
<button class="btn btn-fuel" onclick="updateFuelMap()">Update Fuel Map</button>
<button class="btn btn-success" onclick="autoTuneFuel()">Auto-Tune Fuel</button>
<button class="btn btn-info" onclick="smoothFuelMap()">Smooth Fuel Map</button>
<button class="btn btn-warning" onclick="resetFuelMap()">Reset Fuel Map</button>
<button class="btn btn-danger" onclick="flashFuelToECU()">Flash Fuel to ECU</button>
</div>
<div class="control-group" style="margin-top: 15px;">
<button class="btn btn-primary" onclick="addFuelRPMRow()">Add RPM Point</button>
<button class="btn btn-primary" onclick="removeFuelRPMRow()">Remove RPM Point</button>
<button class="btn btn-info" onclick="exportFuelMap()">Export Fuel Map</button>
<button class="btn btn-info" onclick="importFuelMap()">Import Fuel Map</button>
</div>
</div>
<!-- Ignition Map Editor -->
<div id="ignitionMapEditor" class="map-editor-section">
<div class="map-controls">
<div>
<strong>Editing:</strong> <span id="editingIgnitionMapName">Base Ignition Map</span>
</div>
<div class="view-toggle">
<button class="active" onclick="switchIgnitionView('table')">Table View</button>
<button onclick="switchIgnitionView('3d')">3D View</button>
<button onclick="switchIgnitionView('graph')">Graph View</button>
</div>
<div>
<button class="btn btn-ignition" onclick="saveCurrentIgnitionMap()">Save Ignition Map</button>
<button class="btn btn-info" onclick="exportIgnitionMap()">Export Ignition Map</button>
</div>
</div>
<!-- Table View -->
<div id="ignitionTableView">
<div class="map-container">
<table class="map-table" id="ignitionMapTable">
<thead id="ignitionMapHeader">
<!-- Header will be populated by JavaScript -->
</thead>
<tbody id="ignitionMapBody">
<!-- Ignition map will be populated by JavaScript -->
</tbody>
</table>
</div>
</div>
<!-- 3D View -->
<div id="ignitionView3d" style="display: none;">
<div class="map-3d" id="ignition3dContainer">
<div class="viz-controls">
<button onclick="resetIgnition3DView()">Reset View</button>
<button onclick="toggleIgnitionWireframe()">Wireframe</button>
</div>
</div>
</div>
<!-- Graph View -->
<div id="ignitionViewGraph" style="display: none;">
<div class="map-graph">
<canvas id="ignitionGraphCanvas"></canvas>
</div>
</div>
<div class="control-group" style="margin-top: 15px;">
<button class="btn btn-ignition" onclick="updateIgnitionMap()">Update Ignition Map</button>
<button class="btn btn-success" onclick="autoTuneIgnition()">Auto-Tune Ignition</button>
<button class="btn btn-info" onclick="smoothIgnitionMap()">Smooth Ignition Map</button>
<button class="btn btn-warning" onclick="resetIgnitionMap()">Reset Ignition Map</button>
<button class="btn btn-danger" onclick="flashIgnitionToECU()">Flash Ignition to ECU</button>
</div>
<div class="control-group" style="margin-top: 15px;">
<button class="btn btn-primary" onclick="addIgnitionRPMRow()">Add RPM Point</button>
<button class="btn btn-primary" onclick="removeIgnitionRPMRow()">Remove RPM Point</button>
<button class="btn btn-info" onclick="exportIgnitionMap()">Export Ignition Map</button>
<button class="btn btn-info" onclick="importIgnitionMap()">Import Ignition Map</button>
</div>
</div>
</div>
</div>
<!-- Upload Dialog -->
<div id="uploadDialog" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; justify-content: center; align-items: center;">
<div style="background: white; padding: 25px; border-radius: 15px; width: 90%; max-width: 500px;">
<h3>Upload Custom Map</h3>
<div style="margin: 15px 0;">
<input type="file" id="mapFile" accept=".json,.bin,.hex" style="margin: 10px 0;">
<div style="margin: 10px 0;">
<label>Map Type:</label>
<select id="mapType" style="width: 100%; padding: 8px; margin: 5px 0; border: 1px solid #ddd; border-radius: 4px;">
<option value="fuel">Fuel Map</option>
<option value="ignition">Ignition Map</option>
</select>
</div>
<div style="margin: 10px 0;">
<label>Map Name:</label>
<input type="text" id="newMapName" style="width: 100%; padding: 8px; margin: 5px 0; border: 1px solid #ddd; border-radius: 4px;">
</div>
<div style="margin: 10px 0;">
<label>Description:</label>
<textarea id="newMapDescription" style="width: 100%; padding: 8px; margin: 5px 0; border: 1px solid #ddd; border-radius: 4px; height: 80px;"></textarea>
</div>
</div>
<div class="control-group">
<button class="btn btn-primary" onclick="uploadMap()">Upload</button>
<button class="btn btn-danger" onclick="hideUploadDialog()">Cancel</button>
</div>
</div>
</div>
<script>
let fuelMapData = {};
let ignitionMapData = {};
let currentRpmLimit = 8000;
let currentRpmStep = 250;
let currentLoadStep = 10;
let currentFuelView = 'table';
let currentIgnitionView = 'table';
let currentMapType = 'fuel';
let currentEditor = 'fuel';
// Three.js variables
let fuelScene, fuelCamera, fuelRenderer, fuelMesh;
let ignitionScene, ignitionCamera, ignitionRenderer, ignitionMesh;
let fuelWireframe = false;
let ignitionWireframe = false;
// Chart.js variables
let fuelChart = null;
let ignitionChart = null;
// Fuel Map Management
let fuelMaps = {
'base': {
name: 'Base Fuel Map',
description: 'Default 4-cylinder fuel map',
type: 'base',
editable: true,
data: []
},
'economy': {
name: 'Economy',
description: 'Fuel efficiency focused map',
type: 'base',
editable: true,
data: []
},
'race': {
name: 'Race',
description: 'Aggressive race tuning',
type: 'base',
editable: true,
data: []
}
};
// Ignition Map Management
let ignitionMaps = {
'base': {
name: 'Base Ignition Map',
description: 'Default 4-cylinder ignition map',
type: 'base',
editable: true,
data: []
},
'economy': {
name: 'Economy',
description: 'Fuel efficiency focused ignition',
type: 'base',
editable: true,
data: []
},
'race': {
name: 'Race',
description: 'Aggressive race ignition',
type: 'base',
editable: true,
data: []
}
};
let currentFuelMapId = 'base';
let currentIgnitionMapId = 'base';
// Initialize fuel map
function initializeFuelMap() {
loadFuelMapFromECU();
}
// Initialize ignition map
function initializeIgnitionMap() {
loadIgnitionMapFromECU();
}
function loadFuelMapFromECU() {
fetch('/fuelMap')
.then(response => response.json())
.then(data => {
fuelMapData = data;
currentRpmLimit = data.rpmLimit || 8000;
currentRpmStep = data.rpmStep || 250;
currentLoadStep = data.loadStep || 10;
renderFuelMap();
if (currentFuelView === '3d') {
initFuel3DView();
} else if (currentFuelView === 'graph') {
initFuelGraph();
}
})
.catch(error => {
console.error('Error loading fuel map:', error);
generateDefaultFuelMap();
});
}
function loadIgnitionMapFromECU() {
fetch('/ignitionMap')
.then(response => response.json())
.then(data => {
ignitionMapData = data;
renderIgnitionMap();
if (currentIgnitionView === '3d') {
initIgnition3DView();
} else if (currentIgnitionView === 'graph') {
initIgnitionGraph();
}
})
.catch(error => {
console.error('Error loading ignition map:', error);
generateDefaultIgnitionMap();
});
}
function generateDefaultFuelMap() {
fuelMapData = {
rpmPoints: [],
loadPoints: [],
cells: {},
rpmLimit: 8000,
rpmStep: 250,
loadStep: 10
};
// Generate RPM points from 0 to limit
for (let rpm = 0; rpm <= currentRpmLimit; rpm += currentRpmStep) {
fuelMapData.rpmPoints.push(rpm);
}
// Generate load points from 0 to 100%
for (let load = 0; load <= 100; load += currentLoadStep) {
fuelMapData.loadPoints.push(load);
}
// Initialize cells with default values
fuelMapData.rpmPoints.forEach(rpm => {
fuelMapData.cells[rpm] = {};
fuelMapData.loadPoints.forEach(load => {
// Base calculation for pulse width
let baseValue = 800 + (rpm * 0.1) + (load * 8);
fuelMapData.cells[rpm][load] = Math.round(baseValue);
});
});
renderFuelMap();
}
function generateDefaultIgnitionMap() {
ignitionMapData = {
rpmPoints: [],
loadPoints: [],
cells: {},
rpmLimit: 8000,
rpmStep: 250,
loadStep: 10
};
// Generate RPM points from 0 to limit
for (let rpm = 0; rpm <= currentRpmLimit; rpm += currentRpmStep) {
ignitionMapData.rpmPoints.push(rpm);
}
// Generate load points from 0 to 100%
for (let load = 0; load <= 100; load += currentLoadStep) {
ignitionMapData.loadPoints.push(load);
}
// Initialize cells with default values
ignitionMapData.rpmPoints.forEach(rpm => {
ignitionMapData.cells[rpm] = {};
ignitionMapData.loadPoints.forEach(load => {
// Base calculation for ignition advance
let baseValue = 10 + (rpm * 0.001) + (load * 0.1);
ignitionMapData.cells[rpm][load] = parseFloat(baseValue.toFixed(1));
});
});
renderIgnitionMap();
}
function renderFuelMap() {
const header = document.getElementById('fuelMapHeader');
const body = document.getElementById('fuelMapBody');
// Clear existing content
header.innerHTML = '';
body.innerHTML = '';
// Create header row
let headerRow = '<tr><th>RPM/Load</th>';
fuelMapData.loadPoints.forEach(load => {
headerRow += `<th>${load}%</th>`;
});
headerRow += '</tr>';
header.innerHTML = headerRow;
// Create data rows
fuelMapData.rpmPoints.forEach(rpm => {
let row = `<tr><td><strong>${rpm}</strong></td>`;
fuelMapData.loadPoints.forEach(load => {
const value = fuelMapData.cells[rpm] && fuelMapData.cells[rpm][load] ? fuelMapData.cells[rpm][load] : 0;
row += `<td><input type="number" value="${value}" data-rpm="${rpm}" data-load="${load}"></td>`;
});
row += '</tr>';
body.innerHTML += row;
});
// Update UI info
document.getElementById('selectedFuelMap').textContent = 'Base Fuel Map';
}
function renderIgnitionMap() {
const header = document.getElementById('ignitionMapHeader');
const body = document.getElementById('ignitionMapBody');
// Clear existing content
header.innerHTML = '';
body.innerHTML = '';
// Create header row
let headerRow = '<tr><th>RPM/Load</th>';
ignitionMapData.loadPoints.forEach(load => {
headerRow += `<th>${load}%</th>`;
});
headerRow += '</tr>';
header.innerHTML = headerRow;
// Create data rows
ignitionMapData.rpmPoints.forEach(rpm => {
let row = `<tr><td><strong>${rpm}</strong></td>`;
ignitionMapData.loadPoints.forEach(load => {
const value = ignitionMapData.cells[rpm] && ignitionMapData.cells[rpm][load] ? ignitionMapData.cells[rpm][load] : 0;
row += `<td><input type="number" step="0.1" value="${value}" data-rpm="${rpm}" data-load="${load}"></td>`;
});
row += '</tr>';
body.innerHTML += row;
});
// Update UI info
document.getElementById('selectedIgnitionMap').textContent = 'Base Ignition Map';
}
// 3D Visualization Functions
function initFuel3DView() {
const container = document.getElementById('fuel3dContainer');
container.innerHTML = '<canvas id="fuel3dCanvas"></canvas>';
const canvas = document.getElementById('fuel3dCanvas');
canvas.width = container.clientWidth;
canvas.height = container.clientHeight;
// Create scene
fuelScene = new THREE.Scene();
fuelScene.background = new THREE.Color(0x1a1a1a);
// Create camera
fuelCamera = new THREE.PerspectiveCamera(75, canvas.width / canvas.height, 0.1, 1000);
fuelCamera.position.set(50, 50, 80);
fuelCamera.lookAt(0, 0, 0);
// Create renderer
fuelRenderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true });
fuelRenderer.setSize(canvas.width, canvas.height);
// Add lights
const ambientLight = new THREE.AmbientLight(0x404040);
fuelScene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(50, 50, 50);
fuelScene.add(directionalLight);
// Create grid helper
const gridHelper = new THREE.GridHelper(100, 20, 0x444444, 0x222222);
fuelScene.add(gridHelper);
// Create axes helper
const axesHelper = new THREE.AxesHelper(30);
fuelScene.add(axesHelper);
// Create the surface
createFuelSurface();
// Add orbit controls
const controls = new THREE.OrbitControls(fuelCamera, fuelRenderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
// Animation loop
function animate() {
requestAnimationFrame(animate);
controls.update();
fuelRenderer.render(fuelScene, fuelCamera);
}
animate();
// Handle window resize
window.addEventListener('resize', function() {
canvas.width = container.clientWidth;
canvas.height = container.clientHeight;
fuelCamera.aspect = canvas.width / canvas.height;
fuelCamera.updateProjectionMatrix();
fuelRenderer.setSize(canvas.width, canvas.height);
});
}
function createFuelSurface() {
// Remove existing mesh
if (fuelMesh) {
fuelScene.remove(fuelMesh);
}
const rpmPoints = fuelMapData.rpmPoints;
const loadPoints = fuelMapData.loadPoints;
if (rpmPoints.length === 0 || loadPoints.length === 0) return;
// Create geometry
const geometry = new THREE.PlaneGeometry(80, 80, rpmPoints.length - 1, loadPoints.length - 1);
const positions = geometry.attributes.position.array;
// Scale and position the surface
const rpmScale = 80 / Math.max(...rpmPoints);
const loadScale = 80 / 100;
const valueScale = 0.1; // Scale for pulse width values
for (let i = 0; i < positions.length; i += 3) {
const vertexIndex = i / 3;
const x = positions[i];
const y = positions[i + 1];
// Convert geometry coordinates to map indices
const rpmIndex = Math.round((x + 40) / 80 * (rpmPoints.length - 1));
const loadIndex = Math.round((y + 40) / 80 * (loadPoints.length - 1));
if (rpmIndex >= 0 && rpmIndex < rpmPoints.length && loadIndex >= 0 && loadIndex < loadPoints.length) {
const rpm = rpmPoints[rpmIndex];
const load = loadPoints[loadIndex];
const value = fuelMapData.cells[rpm] && fuelMapData.cells[rpm][load] ? fuelMapData.cells[rpm][load] : 0;
positions[i + 2] = value * valueScale; // Set Z position based on pulse width
}
}
geometry.computeVertexNormals();
// Create material
const material = new THREE.MeshPhongMaterial({
color: 0xe74c3c,
wireframe: fuelWireframe,
side: THREE.DoubleSide,
transparent: true,
opacity: 0.8
});
// Create mesh
fuelMesh = new THREE.Mesh(geometry, material);
fuelMesh.rotation.x = -Math.PI / 2;
fuelScene.add(fuelMesh);
// Add labels
add3DLabels(fuelScene, 'Fuel Map', 'RPM', 'Load', 'Pulse Width (µs)');
}
function initIgnition3DView() {
const container = document.getElementById('ignition3dContainer');
container.innerHTML = '<canvas id="ignition3dCanvas"></canvas>';
const canvas = document.getElementById('ignition3dCanvas');
canvas.width = container.clientWidth;
canvas.height = container.clientHeight;
// Create scene
ignitionScene = new THREE.Scene();
ignitionScene.background = new THREE.Color(0x1a1a1a);
// Create camera
ignitionCamera = new THREE.PerspectiveCamera(75, canvas.width / canvas.height, 0.1, 1000);
ignitionCamera.position.set(50, 50, 80);
ignitionCamera.lookAt(0, 0, 0);
// Create renderer
ignitionRenderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true });
ignitionRenderer.setSize(canvas.width, canvas.height);
// Add lights
const ambientLight = new THREE.AmbientLight(0x404040);
ignitionScene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(50, 50, 50);
ignitionScene.add(directionalLight);
// Create grid helper
const gridHelper = new THREE.GridHelper(100, 20, 0x444444, 0x222222);
ignitionScene.add(gridHelper);
// Create axes helper
const axesHelper = new THREE.AxesHelper(30);
ignitionScene.add(axesHelper);
// Create the surface
createIgnitionSurface();
// Add orbit controls
const controls = new THREE.OrbitControls(ignitionCamera, ignitionRenderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
// Animation loop
function animate() {
requestAnimationFrame(animate);
controls.update();
ignitionRenderer.render(ignitionScene, ignitionCamera);
}
animate();
// Handle window resize
window.addEventListener('resize', function() {
canvas.width = container.clientWidth;
canvas.height = container.clientHeight;
ignitionCamera.aspect = canvas.width / canvas.height;
ignitionCamera.updateProjectionMatrix();
ignitionRenderer.setSize(canvas.width, canvas.height);
});
}
function createIgnitionSurface() {
// Remove existing mesh
if (ignitionMesh) {
ignitionScene.remove(ignitionMesh);
}
const rpmPoints = ignitionMapData.rpmPoints;
const loadPoints = ignitionMapData.loadPoints;
if (rpmPoints.length === 0 || loadPoints.length === 0) return;
// Create geometry
const geometry = new THREE.PlaneGeometry(80, 80, rpmPoints.length - 1, loadPoints.length - 1);
const positions = geometry.attributes.position.array;
// Scale and position the surface
const rpmScale = 80 / Math.max(...rpmPoints);
const loadScale = 80 / 100;
const valueScale = 2; // Scale for ignition advance values
for (let i = 0; i < positions.length; i += 3) {
const vertexIndex = i / 3;
const x = positions[i];
const y = positions[i + 1];
// Convert geometry coordinates to map indices
const rpmIndex = Math.round((x + 40) / 80 * (rpmPoints.length - 1));
const loadIndex = Math.round((y + 40) / 80 * (loadPoints.length - 1));
if (rpmIndex >= 0 && rpmIndex < rpmPoints.length && loadIndex >= 0 && loadIndex < loadPoints.length) {
const rpm = rpmPoints[rpmIndex];
const load = loadPoints[loadIndex];
const value = ignitionMapData.cells[rpm] && ignitionMapData.cells[rpm][load] ? ignitionMapData.cells[rpm][load] : 0;
positions[i + 2] = value * valueScale; // Set Z position based on ignition advance
}
}
geometry.computeVertexNormals();
// Create material
const material = new THREE.MeshPhongMaterial({
color: 0x3498db,
wireframe: ignitionWireframe,
side: THREE.DoubleSide,
transparent: true,
opacity: 0.8
});
// Create mesh
ignitionMesh = new THREE.Mesh(geometry, material);
ignitionMesh.rotation.x = -Math.PI / 2;
ignitionScene.add(ignitionMesh);
// Add labels
add3DLabels(ignitionScene, 'Ignition Map', 'RPM', 'Load', 'Advance (°)');
}
function add3DLabels(scene, title, xLabel, yLabel, zLabel) {
// Simple text labels using HTML would be better, but for simplicity we'll skip detailed labels
}
function resetFuel3DView() {
if (fuelCamera) {
fuelCamera.position.set(50, 50, 80);
fuelCamera.lookAt(0, 0, 0);
}
}
function resetIgnition3DView() {
if (ignitionCamera) {
ignitionCamera.position.set(50, 50, 80);
ignitionCamera.lookAt(0, 0, 0);
}
}
function toggleFuelWireframe() {
fuelWireframe = !fuelWireframe;
if (fuelMesh) {
fuelMesh.material.wireframe = fuelWireframe;
}
}
function toggleIgnitionWireframe() {
ignitionWireframe = !ignitionWireframe;
if (ignitionMesh) {
ignitionMesh.material.wireframe = ignitionWireframe;
}
}
// Graph Visualization Functions
function initFuelGraph() {
const ctx = document.getElementById('fuelGraphCanvas').getContext('2d');
if (fuelChart) {
fuelChart.destroy();
}
const rpmPoints = fuelMapData.rpmPoints;
const loadPoints = fuelMapData.loadPoints;
if (rpmPoints.length === 0 || loadPoints.length === 0) return;
const datasets = [];
const loadColors = [
'#ff4444', '#ff8844', '#ffcc44', '#ffff44',
'#ccff44', '#88ff44', '#44ff44', '#44ff88',
'#44ffcc', '#44ffff', '#4488ff'
];
loadPoints.forEach((load, index) => {
const data = rpmPoints.map(rpm => {
return fuelMapData.cells[rpm] && fuelMapData.cells[rpm][load] ? fuelMapData.cells[rpm][load] : 0;
});
datasets.push({
label: `${load}% Load`,
data: data,
borderColor: loadColors[index % loadColors.length],
backgroundColor: loadColors[index % loadColors.length] + '20',
borderWidth: 2,
tension: 0.4,
fill: false
});
});
fuelChart = new Chart(ctx, {
type: 'line',
data: {
labels: rpmPoints,
datasets: datasets
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: 'Fuel Map - Pulse Width vs RPM',
color: '#333',
font: {
size: 16
}
},
legend: {
position: 'right',
labels: {
color: '#333',
usePointStyle: true
}
}
},
scales: {
x: {
title: {
display: true,
text: 'RPM',
color: '#333'
},
grid: {
color: '#e0e0e0'
},
ticks: {
color: '#666'
}
},
y: {
title: {
display: true,
text: 'Pulse Width (µs)',
color: '#333'
},
grid: {
color: '#e0e0e0'
},
ticks: {
color: '#666'
}
}
}
}
});
}
function initIgnitionGraph() {
const ctx = document.getElementById('ignitionGraphCanvas').getContext('2d');
if (ignitionChart) {
ignitionChart.destroy();
}
const rpmPoints = ignitionMapData.rpmPoints;
const loadPoints = ignitionMapData.loadPoints;
if (rpmPoints.length === 0 || loadPoints.length === 0) return;
const datasets = [];
const loadColors = [
'#4444ff', '#4488ff', '#44ccff', '#44ffff',
'#44ffcc', '#44ff88', '#44ff44', '#88ff44',
'#ccff44', '#ffff44', '#ffcc44'
];
loadPoints.forEach((load, index) => {
const data = rpmPoints.map(rpm => {
return ignitionMapData.cells[rpm] && ignitionMapData.cells[rpm][load] ? ignitionMapData.cells[rpm][load] : 0;
});
datasets.push({
label: `${load}% Load`,
data: data,
borderColor: loadColors[index % loadColors.length],
backgroundColor: loadColors[index % loadColors.length] + '20',
borderWidth: 2,
tension: 0.4,
fill: false
});
});
ignitionChart = new Chart(ctx, {
type: 'line',
data: {
labels: rpmPoints,
datasets: datasets
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: 'Ignition Map - Advance vs RPM',
color: '#333',
font: {
size: 16
}
},
legend: {
position: 'right',
labels: {
color: '#333',
usePointStyle: true
}
}
},
scales: {
x: {
title: {
display: true,
text: 'RPM',
color: '#333'
},
grid: {
color: '#e0e0e0'
},
ticks: {
color: '#666'
}
},
y: {
title: {
display: true,
text: 'Ignition Advance (°)',
color: '#333'
},
grid: {
color: '#e0e0e0'
},
ticks: {
color: '#666'
}
}
}
}
});
}
// View switching functions
function switchFuelView(view) {
currentFuelView = view;
// Update button states
document.querySelectorAll('#fuelMapEditor .view-toggle button').forEach(btn => {
btn.classList.remove('active');
});
event.target.classList.add('active');
// Show/hide views
document.getElementById('fuelTableView').style.display = view === 'table' ? 'block' : 'none';
document.getElementById('fuelView3d').style.display = view === '3d' ? 'block' : 'none';
document.getElementById('fuelViewGraph').style.display = view === 'graph' ? 'block' : 'none';
// Initialize visualization if needed
if (view === '3d' && !fuelRenderer) {
setTimeout(() => initFuel3DView(), 100);
} else if (view === 'graph' && !fuelChart) {
setTimeout(() => initFuelGraph(), 100);
} else if (view === '3d' && fuelRenderer) {
createFuelSurface();
} else if (view === 'graph' && fuelChart) {
fuelChart.update();
}
}
function switchIgnitionView(view) {
currentIgnitionView = view;
// Update button states
document.querySelectorAll('#ignitionMapEditor .view-toggle button').forEach(btn => {
btn.classList.remove('active');
});
event.target.classList.add('active');
// Show/hide views
document.getElementById('ignitionTableView').style.display = view === 'table' ? 'block' : 'none';
document.getElementById('ignitionView3d').style.display = view === '3d' ? 'block' : 'none';
document.getElementById('ignitionViewGraph').style.display = view === 'graph' ? 'block' : 'none';
// Initialize visualization if needed
if (view === '3d' && !ignitionRenderer) {
setTimeout(() => initIgnition3DView(), 100);
} else if (view === 'graph' && !ignitionChart) {
setTimeout(() => initIgnitionGraph(), 100);
} else if (view === '3d' && ignitionRenderer) {
createIgnitionSurface();
} else if (view === 'graph' && ignitionChart) {
ignitionChart.update();
}
}
// Map Selection Functions
function loadFuelMapSelection() {
const grid = document.getElementById('fuelMapSelectionGrid');
grid.innerHTML = '';
for (let mapId in fuelMaps) {
const map = fuelMaps[mapId];
const isActive = mapId === currentFuelMapId;
const mapItem = document.createElement('div');
mapItem.className = `map-item ${isActive ? 'active' : ''}`;
mapItem.onclick = () => selectFuelMap(mapId);
mapItem.innerHTML = `
<h3>${map.name}</h3>
<div class="map-meta">${map.description}</div>
<div class="map-meta">Type: ${map.type} | ${map.editable ? 'Editable' : 'Read-only'}</div>
<div class="map-actions">
<button class="btn btn-primary" onclick="event.stopPropagation(); loadFuelMapForEditing('${mapId}')">Edit</button>
<button class="btn btn-info" onclick="event.stopPropagation(); exportSingleFuelMap('${mapId}')">Export</button>
${map.type === 'custom' ?
`<button class="btn btn-danger" onclick="event.stopPropagation(); deleteFuelMap('${mapId}')">Delete</button>` :
''
}
</div>
`;
grid.appendChild(mapItem);
}
}
function loadIgnitionMapSelection() {
const grid = document.getElementById('ignitionMapSelectionGrid');
grid.innerHTML = '';
for (let mapId in ignitionMaps) {
const map = ignitionMaps[mapId];
const isActive = mapId === currentIgnitionMapId;
const mapItem = document.createElement('div');
mapItem.className = `map-item ${isActive ? 'active' : ''}`;
mapItem.onclick = () => selectIgnitionMap(mapId);
mapItem.innerHTML = `
<h3>${map.name}</h3>
<div class="map-meta">${map.description}</div>
<div class="map-meta">Type: ${map.type} | ${map.editable ? 'Editable' : 'Read-only'}</div>
<div class="map-actions">
<button class="btn btn-primary" onclick="event.stopPropagation(); loadIgnitionMapForEditing('${mapId}')">Edit</button>
<button class="btn btn-info" onclick="event.stopPropagation(); exportSingleIgnitionMap('${mapId}')">Export</button>
${map.type === 'custom' ?
`<button class="btn btn-danger" onclick="event.stopPropagation(); deleteIgnitionMap('${mapId}')">Delete</button>` :
''
}
</div>
`;
grid.appendChild(mapItem);
}
}
function selectFuelMap(mapId) {
currentFuelMapId = mapId;
document.getElementById('selectedFuelMap').textContent = fuelMaps[mapId].name;
document.getElementById('fuelMapStatus').textContent = 'Loaded';
loadFuelMapSelection();
}
function selectIgnitionMap(mapId) {
currentIgnitionMapId = mapId;
document.getElementById('selectedIgnitionMap').textContent = ignitionMaps[mapId].name;
document.getElementById('ignitionMapStatus').textContent = 'Loaded';
loadIgnitionMapSelection();
}
function loadFuelMapForEditing(mapId) {
currentFuelMapId = mapId;
document.getElementById('editingFuelMapName').textContent = fuelMaps[mapId].name;
loadFuelMapFromECU();
}
function loadIgnitionMapForEditing(mapId) {
currentIgnitionMapId = mapId;
document.getElementById('editingIgnitionMapName').textContent = ignitionMaps[mapId].name;
loadIgnitionMapFromECU();
}
function switchMapType(type) {
currentMapType = type;
// Update tab states
document.querySelectorAll('.map-tab').forEach(tab => {
tab.classList.remove('active');
});
event.target.classList.add('active');
// Show/hide sections
document.getElementById('fuelMapSelection').style.display = type === 'fuel' ? 'block' : 'none';
document.getElementById('ignitionMapSelection').style.display = type === 'ignition' ? 'block' : 'none';
}
function switchEditor(type) {
currentEditor = type;
// Update tab states
document.querySelectorAll('.map-tab').forEach(tab => {
tab.classList.remove('active');
});
event.target.classList.add('active');
// Show/hide sections
document.getElementById('fuelMapEditor').style.display = type === 'fuel' ? 'block' : 'none';
document.getElementById('ignitionMapEditor').style.display = type === 'ignition' ? 'block' : 'none';
}
function showUploadDialog() {
document.getElementById('uploadDialog').style.display = 'flex';
}
function hideUploadDialog() {
document.getElementById('uploadDialog').style.display = 'none';
}
function uploadMap() {
const fileInput = document.getElementById('mapFile');
const typeInput = document.getElementById('mapType');
const nameInput = document.getElementById('newMapName');
const descInput = document.getElementById('newMapDescription');
if (!nameInput.value) {
alert('Please enter a map name');
return;
}
const mapType = typeInput.value;
const newMapId = nameInput.value.toLowerCase().replace(/\s+/g, '_');
if (mapType === 'fuel') {
fuelMaps[newMapId] = {
name: nameInput.value,
description: descInput.value,
type: 'custom',
editable: true,
data: fuelMapData
};
loadFuelMapSelection();
} else {
ignitionMaps[newMapId] = {
name: nameInput.value,
description: descInput.value,
type: 'custom',
editable: true,
data: ignitionMapData
};
loadIgnitionMapSelection();
}
// Reset form
fileInput.value = '';
nameInput.value = '';
descInput.value = '';
hideUploadDialog();
alert('Map uploaded successfully!');
}
function createNewMap() {
const type = currentMapType;
const name = prompt('Enter a name for the new ' + type + ' map:');
if (!name) return;
const newMapId = name.toLowerCase().replace(/\s+/g, '_');
if (type === 'fuel') {
fuelMaps[newMapId] = {
name: name,
description: 'Custom fuel map',
type: 'custom',
editable: true,
data: fuelMapData
};
loadFuelMapSelection();
selectFuelMap(newMapId);
} else {
ignitionMaps[newMapId] = {
name: name,
description: 'Custom ignition map',
type: 'custom',
editable: true,
data: ignitionMapData
};
loadIgnitionMapSelection();
selectIgnitionMap(newMapId);
}
}
function deleteFuelMap(mapId) {
if (confirm(`Are you sure you want to delete "${fuelMaps[mapId].name}"?`)) {
delete fuelMaps[mapId