Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- /*******************************************************
- * 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];
- if (currentFuelMapId === mapId) {
- currentFuelMapId = 'base';
- selectFuelMap(currentFuelMapId);
- }
- loadFuelMapSelection();
- }
- }
- function deleteIgnitionMap(mapId) {
- if (confirm(`Are you sure you want to delete "${ignitionMaps[mapId].name}"?`)) {
- delete ignitionMaps[mapId];
- if (currentIgnitionMapId === mapId) {
- currentIgnitionMapId = 'base';
- selectIgnitionMap(currentIgnitionMapId);
- }
- loadIgnitionMapSelection();
- }
- }
- function updateFuelMap() {
- const inputs = document.querySelectorAll('#fuelMapBody input');
- const updates = [];
- inputs.forEach(input => {
- const rpm = parseInt(input.dataset.rpm);
- const load = parseInt(input.dataset.load);
- const value = parseInt(input.value) || 0;
- if (!fuelMapData.cells[rpm]) fuelMapData.cells[rpm] = {};
- fuelMapData.cells[rpm][load] = value;
- updates.push({ rpm, load, pulseWidth: value });
- });
- // Send updates to ECU
- fetch('/fuelMap/update', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ updates: updates })
- })
- .then(() => {
- alert('Fuel map updated successfully!');
- // Update visualizations
- if (currentFuelView === '3d') {
- createFuelSurface();
- } else if (currentFuelView === 'graph') {
- initFuelGraph();
- }
- })
- .catch(error => console.error('Error updating fuel map:', error));
- }
- function updateIgnitionMap() {
- const inputs = document.querySelectorAll('#ignitionMapBody input');
- const updates = [];
- inputs.forEach(input => {
- const rpm = parseInt(input.dataset.rpm);
- const load = parseInt(input.dataset.load);
- const value = parseFloat(input.value) || 0;
- if (!ignitionMapData.cells[rpm]) ignitionMapData.cells[rpm] = {};
- ignitionMapData.cells[rpm][load] = value;
- updates.push({ rpm, load, advance: value });
- });
- // Send updates to ECU
- fetch('/ignitionMap/update', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ updates: updates })
- })
- .then(() => {
- alert('Ignition map updated successfully!');
- // Update visualizations
- if (currentIgnitionView === '3d') {
- createIgnitionSurface();
- } else if (currentIgnitionView === 'graph') {
- initIgnitionGraph();
- }
- })
- .catch(error => console.error('Error updating ignition map:', error));
- }
- function autoTuneFuel() {
- if (confirm('Auto-tune will adjust fuel map based on current AFR readings. Continue?')) {
- fetch('/autotune/fuel')
- .then(() => {
- alert('Fuel auto-tune completed!');
- loadFuelMapFromECU();
- })
- .catch(error => console.error('Error auto-tuning fuel:', error));
- }
- }
- function autoTuneIgnition() {
- if (confirm('Auto-tune will adjust ignition map based on current engine knock detection. Continue?')) {
- fetch('/autotune/ignition')
- .then(() => {
- alert('Ignition auto-tune completed!');
- loadIgnitionMapFromECU();
- })
- .catch(error => console.error('Error auto-tuning ignition:', error));
- }
- }
- function smoothFuelMap() {
- if (confirm('Smooth transitions between fuel map cells?')) {
- fetch('/smoothMap/fuel')
- .then(() => {
- alert('Fuel map smoothed!');
- loadFuelMapFromECU();
- })
- .catch(error => console.error('Error smoothing fuel map:', error));
- }
- }
- function smoothIgnitionMap() {
- if (confirm('Smooth transitions between ignition map cells?')) {
- fetch('/smoothMap/ignition')
- .then(() => {
- alert('Ignition map smoothed!');
- loadIgnitionMapFromECU();
- })
- .catch(error => console.error('Error smoothing ignition map:', error));
- }
- }
- function resetFuelMap() {
- if (confirm('Reset fuel map to default values?')) {
- fetch('/resetMap/fuel')
- .then(() => {
- alert('Fuel map reset!');
- loadFuelMapFromECU();
- })
- .catch(error => console.error('Error resetting fuel map:', error));
- }
- }
- function resetIgnitionMap() {
- if (confirm('Reset ignition map to default values?')) {
- fetch('/resetMap/ignition')
- .then(() => {
- alert('Ignition map reset!');
- loadIgnitionMapFromECU();
- })
- .catch(error => console.error('Error resetting ignition map:', error));
- }
- }
- function flashFuelToECU() {
- if (confirm('Flash current fuel map to ECU?')) {
- fetch('/flashMap/fuel')
- .then(() => {
- alert('Fuel map flashed to ECU!');
- })
- .catch(error => console.error('Error flashing fuel map:', error));
- }
- }
- function flashIgnitionToECU() {
- if (confirm('Flash current ignition map to ECU?')) {
- fetch('/flashMap/ignition')
- .then(() => {
- alert('Ignition map flashed to ECU!');
- })
- .catch(error => console.error('Error flashing ignition map:', error));
- }
- }
- function addFuelRPMRow() {
- const newRPM = prompt('Enter new RPM point:');
- if (newRPM && !isNaN(newRPM)) {
- const rpm = parseInt(newRPM);
- if (rpm >= 0 && rpm <= 12000) {
- fetch('/addRPM/fuel', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ rpm: rpm })
- })
- .then(() => {
- loadFuelMapFromECU();
- })
- .catch(error => console.error('Error adding fuel RPM point:', error));
- }
- }
- }
- function addIgnitionRPMRow() {
- const newRPM = prompt('Enter new RPM point:');
- if (newRPM && !isNaN(newRPM)) {
- const rpm = parseInt(newRPM);
- if (rpm >= 0 && rpm <= 12000) {
- fetch('/addRPM/ignition', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ rpm: rpm })
- })
- .then(() => {
- loadIgnitionMapFromECU();
- })
- .catch(error => console.error('Error adding ignition RPM point:', error));
- }
- }
- }
- function removeFuelRPMRow() {
- const rpm = prompt('Enter RPM point to remove:');
- if (rpm && !isNaN(rpm)) {
- fetch('/removeRPM/fuel', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ rpm: parseInt(rpm) })
- })
- .then(() => {
- loadFuelMapFromECU();
- })
- .catch(error => console.error('Error removing fuel RPM point:', error));
- }
- }
- function removeIgnitionRPMRow() {
- const rpm = prompt('Enter RPM point to remove:');
- if (rpm && !isNaN(rpm)) {
- fetch('/removeRPM/ignition', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ rpm: parseInt(rpm) })
- })
- .then(() => {
- loadIgnitionMapFromECU();
- })
- .catch(error => console.error('Error removing ignition RPM point:', error));
- }
- }
- function exportFuelMap() {
- const mapData = JSON.stringify(fuelMapData, null, 2);
- const blob = new Blob([mapData], { type: 'application/json' });
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = 'fuel_map.json';
- a.click();
- URL.revokeObjectURL(url);
- }
- function exportIgnitionMap() {
- const mapData = JSON.stringify(ignitionMapData, null, 2);
- const blob = new Blob([mapData], { type: 'application/json' });
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = 'ignition_map.json';
- a.click();
- URL.revokeObjectURL(url);
- }
- function exportSingleFuelMap(mapId) {
- const mapData = JSON.stringify(fuelMaps[mapId].data, null, 2);
- const blob = new Blob([mapData], { type: 'application/json' });
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = `${fuelMaps[mapId].name.replace(/\s+/g, '_')}.json`;
- a.click();
- URL.revokeObjectURL(url);
- }
- function exportSingleIgnitionMap(mapId) {
- const mapData = JSON.stringify(ignitionMaps[mapId].data, null, 2);
- const blob = new Blob([mapData], { type: 'application/json' });
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = `${ignitionMaps[mapId].name.replace(/\s+/g, '_')}.json`;
- a.click();
- URL.revokeObjectURL(url);
- }
- function importFuelMap() {
- const input = document.createElement('input');
- input.type = 'file';
- input.accept = '.json';
- input.onchange = e => {
- const file = e.target.files[0];
- const reader = new FileReader();
- reader.onload = event => {
- try {
- const importedData = JSON.parse(event.target.result);
- fuelMapData = importedData;
- renderFuelMap();
- if (currentFuelView === '3d') {
- createFuelSurface();
- } else if (currentFuelView === 'graph') {
- initFuelGraph();
- }
- alert('Fuel map imported successfully!');
- } catch (error) {
- alert('Error importing fuel map: ' + error.message);
- }
- };
- reader.readAsText(file);
- };
- input.click();
- }
- function importIgnitionMap() {
- const input = document.createElement('input');
- input.type = 'file';
- input.accept = '.json';
- input.onchange = e => {
- const file = e.target.files[0];
- const reader = new FileReader();
- reader.onload = event => {
- try {
- const importedData = JSON.parse(event.target.result);
- ignitionMapData = importedData;
- renderIgnitionMap();
- if (currentIgnitionView === '3d') {
- createIgnitionSurface();
- } else if (currentIgnitionView === 'graph') {
- initIgnitionGraph();
- }
- alert('Ignition map imported successfully!');
- } catch (error) {
- alert('Error importing ignition map: ' + error.message);
- }
- };
- reader.readAsText(file);
- };
- input.click();
- }
- function saveCurrentFuelMap() {
- updateFuelMap();
- alert(`"${fuelMaps[currentFuelMapId].name}" saved successfully!`);
- }
- function saveCurrentIgnitionMap() {
- updateIgnitionMap();
- alert(`"${ignitionMaps[currentIgnitionMapId].name}" saved successfully!`);
- }
- // Lighting control functions
- function toggleHeadlights() {
- fetch('/lights/headlight/toggle')
- .then(updateSensorData)
- .catch(error => console.error('Error toggling headlights:', error));
- }
- function toggleHighbeams() {
- fetch('/lights/highbeam/toggle')
- .then(updateSensorData)
- .catch(error => console.error('Error toggling highbeams:', error));
- }
- function toggleLeftTurn() {
- fetch('/lights/leftturn/toggle')
- .then(updateSensorData)
- .catch(error => console.error('Error toggling left turn:', error));
- }
- function toggleRightTurn() {
- fetch('/lights/rightturn/toggle')
- .then(updateSensorData)
- .catch(error => console.error('Error toggling right turn:', error));
- }
- function toggleBrakeLights() {
- fetch('/lights/brake/toggle')
- .then(updateSensorData)
- .catch(error => console.error('Error toggling brake lights:', error));
- }
- function toggleHazards() {
- fetch('/lights/hazard/toggle')
- .then(updateSensorData)
- .catch(error => console.error('Error toggling hazards:', error));
- }
- // Existing sensor data and control functions
- function updateSensorData() {
- fetch('/sensorData')
- .then(response => response.json())
- .then(data => {
- // Update sensor values
- document.getElementById('rpmValue').textContent = Math.round(data.rpm);
- document.getElementById('o2Value').textContent = data.o2.toFixed(2);
- document.getElementById('tpsValue').textContent = Math.round(data.tps);
- document.getElementById('mapValue').textContent = Math.round(data.map);
- document.getElementById('tempValue').textContent = Math.round(data.temp);
- document.getElementById('voltageValue').textContent = data.voltage.toFixed(1);
- document.getElementById('activeCylinder').textContent = data.activeCylinder + 1;
- document.getElementById('fuelType').textContent = data.fuelType;
- document.getElementById('currentTune').textContent = data.currentTune;
- // Update cylinder pulses and timing
- document.getElementById('pulse1').textContent = data.pulseWidths[0];
- document.getElementById('pulse2').textContent = data.pulseWidths[1];
- document.getElementById('pulse3').textContent = data.pulseWidths[2];
- document.getElementById('pulse4').textContent = data.pulseWidths[3];
- document.getElementById('timing1').textContent = data.ignitionTiming[0];
- document.getElementById('timing2').textContent = data.ignitionTiming[1];
- document.getElementById('timing3').textContent = data.ignitionTiming[2];
- document.getElementById('timing4').textContent = data.ignitionTiming[3];
- // Update cylinder highlighting
- for (let i = 1; i <= 4; i++) {
- const cylElement = document.getElementById('cyl' + i);
- if (i === data.activeCylinder + 1) {
- cylElement.classList.add('cylinder-active');
- } else {
- cylElement.classList.remove('cylinder-active');
- }
- }
- // Update gauges
- document.getElementById('rpmGauge').style.width = Math.min(data.rpm / 100, 100) + '%';
- document.getElementById('tpsGauge').style.width = data.tps + '%';
- document.getElementById('mapGauge').style.width = data.map + '%';
- document.getElementById('tempGauge').style.width = Math.min(data.temp, 100) + '%';
- document.getElementById('voltageGauge').style.width = Math.min((data.voltage - 10) * 10, 100) + '%';
- document.getElementById('o2Gauge').style.width = ((data.o2 - 6) * 8.33) + '%';
- // Update system status
- const systemStatus = document.getElementById('systemStatusText');
- const systemLight = document.getElementById('systemStatusLight');
- const powerStatus = document.getElementById('powerStatus');
- const powerLight = document.getElementById('powerLight');
- const faultPanel = document.getElementById('faultPanel');
- const faultMessage = document.getElementById('faultMessage');
- if (data.criticalFault) {
- systemStatus.textContent = 'FAULT';
- systemStatus.style.color = '#e74c3c';
- systemLight.className = 'status-indicator status-fault';
- powerStatus.textContent = 'OFF';
- powerLight.className = 'status-indicator status-off';
- faultPanel.style.display = 'block';
- faultMessage.textContent = data.faultMessage;
- } else if (data.systemEnabled) {
- systemStatus.textContent = 'RUNNING';
- systemStatus.style.color = '#27ae60';
- systemLight.className = 'status-indicator status-on';
- powerStatus.textContent = 'ON';
- powerLight.className = 'status-indicator status-on';
- faultPanel.style.display = 'none';
- } else {
- systemStatus.textContent = 'READY';
- systemStatus.style.color = '#f39c12';
- systemLight.className = 'status-indicator status-off';
- powerStatus.textContent = 'OFF';
- powerLight.className = 'status-indicator status-off';
- faultPanel.style.display = 'none';
- }
- // Update lighting status
- document.getElementById('headlightStatus').textContent = data.headlightState ? 'ON' : 'OFF';
- document.getElementById('headlightStatus').className = `light-status ${data.headlightState ? 'status-on' : 'status-off'}`;
- document.getElementById('highbeamStatus').textContent = data.highbeamState ? 'ON' : 'OFF';
- document.getElementById('highbeamStatus').className = `light-status ${data.highbeamState ? 'status-on' : 'status-off'}`;
- document.getElementById('leftTurnStatus').textContent = data.leftTurnState ? 'BLINKING' : 'OFF';
- document.getElementById('leftTurnStatus').className = `light-status ${data.leftTurnState ? 'status-blinking' : 'status-off'}`;
- document.getElementById('rightTurnStatus').textContent = data.rightTurnState ? 'BLINKING' : 'OFF';
- document.getElementById('rightTurnStatus').className = `light-status ${data.rightTurnState ? 'status-blinking' : 'status-off'}`;
- document.getElementById('brakeLightStatus').textContent = data.brakeLightState ? 'ON' : 'OFF';
- document.getElementById('brakeLightStatus').className = `light-status ${data.brakeLightState ? 'status-on' : 'status-off'}`;
- document.getElementById('hazardStatus').textContent = data.hazardState ? 'BLINKING' : 'OFF';
- document.getElementById('hazardStatus').className = `light-status ${data.hazardState ? 'status-blinking' : 'status-off'}`;
- document.getElementById('pumpStatus').textContent = data.fuelPumpRunning ? 'ON' : 'OFF';
- // Update tuning sliders
- document.getElementById('fuelMultiplier').value = data.fuelMultiplier;
- document.getElementById('fuelMultiplierValue').textContent = data.fuelMultiplier.toFixed(2);
- document.getElementById('ignitionAdvance').value = data.ignitionAdvance;
- document.getElementById('ignitionAdvanceValue').textContent = data.ignitionAdvance.toFixed(1);
- document.getElementById('targetAFR').value = data.targetAFR;
- document.getElementById('targetAFRValue').textContent = data.targetAFR.toFixed(1);
- document.getElementById('rpmLimit').value = data.rpmLimit;
- document.getElementById('rpmLimitValue').textContent = data.rpmLimit;
- })
- .catch(error => console.error('Error fetching sensor data:', error));
- }
- // Control functions
- function toggleSystem() {
- fetch('/system/toggle')
- .then(updateSensorData)
- .catch(error => console.error('Error toggling system:', error));
- }
- function toggleFuelPump() {
- fetch('/pump/toggle')
- .then(updateSensorData)
- .catch(error => console.error('Error toggling fuel pump:', error));
- }
- // Preset tune functions
- function applyPreset(tuneName) {
- fetch('/preset/' + tuneName)
- .then(() => {
- const tuneInfo = {
- 'economy': {name: 'Economy', desc: 'Fuel-efficient tuning for maximum mileage'},
- 'performance': {name: 'Performance', desc: 'Balanced performance for street use'},
- 'race': {name: 'Race', desc: 'Aggressive tuning for maximum power'},
- 'methanol': {name: 'Methanol', desc: 'Optimized for methanol fuel (requires ~40% more fuel)'},
- 'gasoline': {name: 'Gasoline', desc: 'Standard tuning for gasoline fuel'}
- };
- document.getElementById('activeTuneName').textContent = tuneInfo[tuneName].name;
- document.getElementById('tuneDescription').textContent = tuneInfo[tuneName].desc;
- updateSensorData();
- })
- .catch(error => console.error('Error applying preset:', error));
- }
- // Tuning functions
- function updateFuelMultiplier(value) {
- document.getElementById('fuelMultiplierValue').textContent = parseFloat(value).toFixed(2);
- fetch('/tuning/fuelMultiplier?value=' + value)
- .catch(error => console.error('Error updating fuel multiplier:', error));
- }
- function updateIgnitionAdvance(value) {
- document.getElementById('ignitionAdvanceValue').textContent = parseFloat(value).toFixed(1);
- fetch('/tuning/ignitionAdvance?value=' + value)
- .catch(error => console.error('Error updating ignition advance:', error));
- }
- function updateTargetAFR(value) {
- document.getElementById('targetAFRValue').textContent = parseFloat(value).toFixed(1);
- fetch('/tuning/targetAFR?value=' + value)
- .catch(error => console.error('Error updating target AFR:', error));
- }
- function updateRPMLimit(value) {
- document.getElementById('rpmLimitValue').textContent = value;
- fetch('/tuning/rpmLimit?value=' + value)
- .then(() => {
- // Reload maps when RPM limit changes
- loadFuelMapFromECU();
- loadIgnitionMapFromECU();
- })
- .catch(error => console.error('Error updating RPM limit:', error));
- }
- function saveTuning() {
- fetch('/tuning/save')
- .then(() => alert('Tuning parameters saved!'))
- .catch(error => console.error('Error saving tuning:', error));
- }
- function loadTuning() {
- fetch('/tuning/load')
- .then(() => {
- alert('Tuning parameters loaded!');
- location.reload();
- })
- .catch(error => console.error('Error loading tuning:', error));
- }
- function resetTuning() {
- if (confirm('Reset all tuning to defaults?')) {
- fetch('/tuning/reset')
- .then(() => {
- alert('Tuning parameters reset!');
- location.reload();
- })
- .catch(error => console.error('Error resetting tuning:', error));
- }
- }
- // Initialize everything when page loads
- document.addEventListener('DOMContentLoaded', function() {
- initializeFuelMap();
- initializeIgnitionMap();
- loadFuelMapSelection();
- loadIgnitionMapSelection();
- selectFuelMap('base');
- selectIgnitionMap('base');
- setInterval(updateSensorData, 500);
- // Set initial tune info
- document.getElementById('activeTuneName').textContent = 'Performance';
- document.getElementById('tuneDescription').textContent = 'Balanced performance for street use';
- });
- </script>
- </body>
- </html>
- )rawliteral";
- // ==================== FUEL & IGNITION MAP MANAGEMENT ====================
- void initializeFuelMap() {
- fuelMap.name = "Base Fuel Map";
- fuelMap.description = "Default 4-cylinder fuel map";
- fuelMap.fuelType = "gasoline";
- // Initialize RPM points from 0 to limit in steps
- fuelMap.rpmPointCount = 0;
- for (int rpm = tuning.minRPM; rpm <= tuning.maxRPM; rpm += tuning.rpmStep) {
- if (fuelMap.rpmPointCount < MAX_RPM_POINTS) {
- fuelMap.rpmPoints[fuelMap.rpmPointCount] = rpm;
- fuelMap.rpmPointCount++;
- }
- }
- // Initialize load points from 0 to 100% in steps
- fuelMap.loadPointCount = 0;
- for (int load = 0; load <= 100; load += tuning.loadStep) {
- if (fuelMap.loadPointCount < MAX_LOAD_POINTS) {
- fuelMap.loadPoints[fuelMap.loadPointCount] = load;
- fuelMap.loadPointCount++;
- }
- }
- // Initialize all cells with default values
- for (int i = 0; i < fuelMap.rpmPointCount; i++) {
- for (int j = 0; j < fuelMap.loadPointCount; j++) {
- int rpm = fuelMap.rpmPoints[i];
- int load = fuelMap.loadPoints[j];
- // Base pulse width calculation
- int basePulse = 800 + (rpm * 0.1) + (load * 8);
- fuelMap.cells[i][j].pulseWidth = basePulse;
- fuelMap.cells[i][j].learned = false;
- fuelMap.cells[i][j].correction = 1.0;
- }
- }
- // Initialize multiple fuel maps
- fuelMapCount = 3;
- // Base map
- fuelMaps[0] = fuelMap;
- // Economy map
- fuelMaps[1] = fuelMap;
- fuelMaps[1].name = "Economy";
- fuelMaps[1].description = "Fuel efficiency focused map";
- for (int i = 0; i < fuelMaps[1].rpmPointCount; i++) {
- for (int j = 0; j < fuelMaps[1].loadPointCount; j++) {
- fuelMaps[1].cells[i][j].pulseWidth *= 0.9; // 10% leaner
- }
- }
- // Race map
- fuelMaps[2] = fuelMap;
- fuelMaps[2].name = "Race";
- fuelMaps[2].description = "Aggressive race tuning";
- for (int i = 0; i < fuelMaps[2].rpmPointCount; i++) {
- for (int j = 0; j < fuelMaps[2].loadPointCount; j++) {
- fuelMaps[2].cells[i][j].pulseWidth *= 1.15; // 15% richer
- }
- }
- }
- void initializeIgnitionMap() {
- ignitionMap.name = "Base Ignition Map";
- ignitionMap.description = "Default 4-cylinder ignition map";
- // Initialize RPM points from 0 to limit in steps
- ignitionMap.rpmPointCount = 0;
- for (int rpm = tuning.minRPM; rpm <= tuning.maxRPM; rpm += tuning.rpmStep) {
- if (ignitionMap.rpmPointCount < MAX_RPM_POINTS) {
- ignitionMap.rpmPoints[ignitionMap.rpmPointCount] = rpm;
- ignitionMap.rpmPointCount++;
- }
- }
- // Initialize load points from 0 to 100% in steps
- ignitionMap.loadPointCount = 0;
- for (int load = 0; load <= 100; load += tuning.loadStep) {
- if (ignitionMap.loadPointCount < MAX_LOAD_POINTS) {
- ignitionMap.loadPoints[ignitionMap.loadPointCount] = load;
- ignitionMap.loadPointCount++;
- }
- }
- // Initialize all cells with default values
- for (int i = 0; i < ignitionMap.rpmPointCount; i++) {
- for (int j = 0; j < ignitionMap.loadPointCount; j++) {
- int rpm = ignitionMap.rpmPoints[i];
- int load = ignitionMap.loadPoints[j];
- // Base ignition advance calculation
- float baseAdvance = 10.0 + (rpm * 0.001) + (load * 0.1);
- ignitionMap.cells[i][j].advance = baseAdvance;
- ignitionMap.cells[i][j].learned = false;
- ignitionMap.cells[i][j].correction = 1.0;
- }
- }
- // Initialize multiple ignition maps
- ignitionMapCount = 3;
- // Base map
- ignitionMaps[0] = ignitionMap;
- // Economy map
- ignitionMaps[1] = ignitionMap;
- ignitionMaps[1].name = "Economy";
- ignitionMaps[1].description = "Fuel efficiency focused ignition";
- for (int i = 0; i < ignitionMaps[1].rpmPointCount; i++) {
- for (int j = 0; j < ignitionMaps[1].loadPointCount; j++) {
- ignitionMaps[1].cells[i][j].advance += 2.0; // More advance for economy
- }
- }
- // Race map
- ignitionMaps[2] = ignitionMap;
- ignitionMaps[2].name = "Race";
- ignitionMaps[2].description = "Aggressive race ignition";
- for (int i = 0; i < ignitionMaps[2].rpmPointCount; i++) {
- for (int j = 0; j < ignitionMaps[2].loadPointCount; j++) {
- ignitionMaps[2].cells[i][j].advance -= 2.0; // Less advance for race (conservative)
- }
- }
- }
- int findRPMIndex(int rpm, bool isFuelMap = true) {
- if (isFuelMap) {
- for (int i = 0; i < fuelMap.rpmPointCount; i++) {
- if (fuelMap.rpmPoints[i] >= rpm) {
- return i;
- }
- }
- return fuelMap.rpmPointCount - 1;
- } else {
- for (int i = 0; i < ignitionMap.rpmPointCount; i++) {
- if (ignitionMap.rpmPoints[i] >= rpm) {
- return i;
- }
- }
- return ignitionMap.rpmPointCount - 1;
- }
- }
- int findLoadIndex(int load, bool isFuelMap = true) {
- if (isFuelMap) {
- for (int i = 0; i < fuelMap.loadPointCount; i++) {
- if (fuelMap.loadPoints[i] >= load) {
- return i;
- }
- }
- return fuelMap.loadPointCount - 1;
- } else {
- for (int i = 0; i < ignitionMap.loadPointCount; i++) {
- if (ignitionMap.loadPoints[i] >= load) {
- return i;
- }
- }
- return ignitionMap.loadPointCount - 1;
- }
- }
- int getFuelPulseWidth(int rpm, int load) {
- if (rpm < tuning.minRPM) rpm = tuning.minRPM;
- if (rpm > tuning.maxRPM) rpm = tuning.maxRPM;
- if (load < 0) load = 0;
- if (load > 100) load = 100;
- int rpmIndex = findRPMIndex(rpm, true);
- int loadIndex = findLoadIndex(load, true);
- int basePulse = fuelMap.cells[rpmIndex][loadIndex].pulseWidth;
- float correction = fuelMap.cells[rpmIndex][loadIndex].correction;
- // Apply tuning adjustments
- float adjustedPulse = basePulse * tuning.fuelMultiplier * correction;
- // Closed-loop correction based on O2 sensor
- float afrError = o2Value - tuning.targetAFR;
- float closedLoopCorrection = 1.0 + (afrError * 0.05);
- closedLoopCorrection = constrain(closedLoopCorrection, 0.7, 1.3);
- adjustedPulse *= closedLoopCorrection;
- return (int)adjustedPulse;
- }
- float getIgnitionAdvance(int rpm, int load) {
- if (rpm < tuning.minRPM) rpm = tuning.minRPM;
- if (rpm > tuning.maxRPM) rpm = tuning.maxRPM;
- if (load < 0) load = 0;
- if (load > 100) load = 100;
- int rpmIndex = findRPMIndex(rpm, false);
- int loadIndex = findLoadIndex(load, false);
- float baseAdvance = ignitionMap.cells[rpmIndex][loadIndex].advance;
- float correction = ignitionMap.cells[rpmIndex][loadIndex].correction;
- // Apply tuning adjustments
- float adjustedAdvance = baseAdvance + (tuning.ignitionAdvance - 12.0);
- adjustedAdvance *= correction;
- // Safety limits
- adjustedAdvance = constrain(adjustedAdvance, 5.0, 35.0);
- return adjustedAdvance;
- }
- // ==================== EEPROM MANAGEMENT ====================
- void saveTuningToEEPROM() {
- EEPROM.begin(8192);
- int addr = 0;
- // Save tuning parameters
- EEPROM.put(addr, tuning);
- addr += sizeof(TuningParams);
- // Save fuel map
- EEPROM.put(addr, fuelMap);
- addr += sizeof(FuelMap);
- // Save ignition map
- EEPROM.put(addr, ignitionMap);
- addr += sizeof(IgnitionMap);
- // Save multiple fuel maps
- EEPROM.put(addr, fuelMapCount);
- addr += sizeof(int);
- for (int i = 0; i < fuelMapCount; i++) {
- EEPROM.put(addr, fuelMaps[i]);
- addr += sizeof(FuelMap);
- }
- // Save multiple ignition maps
- EEPROM.put(addr, ignitionMapCount);
- addr += sizeof(int);
- for (int i = 0; i < ignitionMapCount; i++) {
- EEPROM.put(addr, ignitionMaps[i]);
- addr += sizeof(IgnitionMap);
- }
- EEPROM.put(addr, currentFuelMapIndex);
- addr += sizeof(int);
- EEPROM.put(addr, currentIgnitionMapIndex);
- EEPROM.commit();
- EEPROM.end();
- Serial.println("Tuning saved to EEPROM");
- }
- void loadTuningFromEEPROM() {
- EEPROM.begin(8192);
- int addr = 0;
- // Load tuning parameters
- EEPROM.get(addr, tuning);
- addr += sizeof(TuningParams);
- // Load fuel map
- EEPROM.get(addr, fuelMap);
- addr += sizeof(FuelMap);
- // Load ignition map
- EEPROM.get(addr, ignitionMap);
- addr += sizeof(IgnitionMap);
- // Load multiple fuel maps
- EEPROM.get(addr, fuelMapCount);
- addr += sizeof(int);
- for (int i = 0; i < fuelMapCount && i < MAX_FUEL_MAPS; i++) {
- EEPROM.get(addr, fuelMaps[i]);
- addr += sizeof(FuelMap);
- }
- // Load multiple ignition maps
- EEPROM.get(addr, ignitionMapCount);
- addr += sizeof(int);
- for (int i = 0; i < ignitionMapCount && i < MAX_IGNITION_MAPS; i++) {
- EEPROM.get(addr, ignitionMaps[i]);
- addr += sizeof(IgnitionMap);
- }
- EEPROM.get(addr, currentFuelMapIndex);
- addr += sizeof(int);
- EEPROM.get(addr, currentIgnitionMapIndex);
- EEPROM.end();
- Serial.println("Tuning loaded from EEPROM");
- }
- // ==================== LIGHTING CONTROL ====================
- void updateLighting() {
- // Headlights
- digitalWrite(PIN_HEADLIGHT, headlightState);
- digitalWrite(PIN_HIGHBEAM, highbeamState && headlightState);
- // Brake lights
- digitalWrite(PIN_BRAKE_LIGHT, brakeLightState);
- // Turn signals with hazard override
- unsigned long currentMillis = millis();
- if (currentMillis - turnSignalPreviousMillis >= turnSignalInterval) {
- turnSignalPreviousMillis = currentMillis;
- turnSignalBlinkState = !turnSignalBlinkState;
- }
- if (hazardState) {
- digitalWrite(PIN_LEFT_TURN, turnSignalBlinkState);
- digitalWrite(PIN_RIGHT_TURN, turnSignalBlinkState);
- } else {
- digitalWrite(PIN_LEFT_TURN, leftTurnState && turnSignalBlinkState);
- digitalWrite(PIN_RIGHT_TURN, rightTurnState && turnSignalBlinkState);
- }
- }
- void toggleHeadlight() {
- headlightState = !headlightState;
- if (!headlightState) highbeamState = false;
- }
- void toggleHighbeam() {
- if (headlightState) {
- highbeamState = !highbeamState;
- }
- }
- void toggleLeftTurn() {
- leftTurnState = !leftTurnState;
- if (leftTurnState) rightTurnState = false;
- }
- void toggleRightTurn() {
- rightTurnState = !rightTurnState;
- if (rightTurnState) leftTurnState = false;
- }
- void toggleBrakeLight() {
- brakeLightState = !brakeLightState;
- }
- void toggleHazard() {
- hazardState = !hazardState;
- }
- // ==================== 4-CYLINDER ENGINE CONTROL ====================
- void IRAM_ATTR rpmInterrupt() {
- unsigned long currentTime = micros();
- rpmPulseTime = currentTime - lastRpmPulse;
- lastRpmPulse = currentTime;
- if (rpmPulseTime > 500) {
- currentRPM = 60000000.0 / rpmPulseTime;
- if (currentRPM > 20000) currentRPM = 0;
- // Advance to next cylinder (1-3-4-2 firing order)
- currentCylinder = (currentCylinder + 1) % 4;
- lastCylinderTime = currentTime;
- }
- }
- void controlInjection() {
- if (!systemEnabled || currentRPM == 0) {
- for (int i = 0; i < 4; i++) {
- digitalWrite(PIN_INJECTOR_1 + i, LOW);
- }
- return;
- }
- unsigned long currentTime = micros();
- int pulseWidth = getFuelPulseWidth(currentRPM, mapValue);
- // Calculate injection timing (sequential injection)
- unsigned long injectionInterval = 120000000 / currentRPM;
- // Fire injector for current cylinder in firing order
- int injectorPin = PIN_INJECTOR_1 + firingOrder[currentCylinder];
- if (currentTime - lastInjectorPulse[currentCylinder] > injectionInterval) {
- digitalWrite(injectorPin, HIGH);
- lastInjectorPulse[currentCylinder] = currentTime;
- currentPulseWidth[currentCylinder] = pulseWidth;
- }
- // Turn off injector after pulse width duration
- if (digitalRead(injectorPin) == HIGH &&
- currentTime - lastInjectorPulse[currentCylinder] > currentPulseWidth[currentCylinder]) {
- digitalWrite(injectorPin, LOW);
- }
- }
- void controlIgnition() {
- if (!systemEnabled || currentRPM == 0) {
- for (int i = 0; i < 4; i++) {
- digitalWrite(PIN_IGNITION_1 + i, LOW);
- }
- return;
- }
- unsigned long currentTime = micros();
- unsigned long ignitionInterval = 120000000 / currentRPM;
- float ignitionAdvance = getIgnitionAdvance(currentRPM, mapValue);
- // Convert advance angle to time (microseconds)
- unsigned long advanceTime = (ignitionAdvance * 1000000) / (currentRPM * 6);
- unsigned long dwellTime = 2000; // 2ms dwell
- // Fire ignition for current cylinder
- int ignitionPin = PIN_IGNITION_1 + ignitionOrder[currentCylinder];
- static unsigned long lastIgnitionTime[4] = {0, 0, 0, 0};
- static float lastIgnitionTiming[4] = {0, 0, 0, 0};
- if (currentTime - lastIgnitionTime[currentCylinder] > ignitionInterval) {
- digitalWrite(ignitionPin, HIGH);
- lastIgnitionTime[currentCylinder] = currentTime;
- lastIgnitionTiming[currentCylinder] = ignitionAdvance;
- }
- if (currentTime - lastIgnitionTime[currentCylinder] > dwellTime) {
- digitalWrite(ignitionPin, LOW);
- }
- }
- // ==================== SENSOR READING ====================
- void readSensors() {
- if(systemEnabled && currentRPM > 0) {
- // Simulate sensor readings
- int tpsRaw = analogRead(PIN_TPS);
- tpsValue = map(tpsRaw, 0, 4095, 0, 100);
- int mapRaw = analogRead(PIN_MAP_SENSOR);
- mapValue = map(tpsRaw, 0, 4095, 10, 90) + (currentRPM * 0.001);
- if(mapValue > 100) mapValue = 100;
- o2Value = 14.7 + (sin(millis() * 0.001) * 0.5);
- engineTemp = 80 + (sin(millis() * 0.0001) * 5);
- batteryVoltage = 12.5 + (sin(millis() * 0.0005) * 0.3);
- } else {
- o2Value = 14.7;
- tpsValue = 0;
- mapValue = 0;
- engineTemp = 20;
- batteryVoltage = 12.5;
- }
- if (micros() - lastRpmPulse > 1000000) {
- currentRPM = 0;
- }
- }
- // ==================== FAIL-SAFE SYSTEM ====================
- bool performSafetyChecks() {
- if (currentRPM > tuning.rpmLimit) {
- faultMessage = "RPM LIMIT EXCEEDED: " + String(currentRPM);
- return false;
- }
- if (engineTemp > tuning.tempLimit) {
- faultMessage = "ENGINE OVERHEAT: " + String(engineTemp) + "C";
- return false;
- }
- if (batteryVoltage < 10.0) {
- faultMessage = "LOW BATTERY: " + String(batteryVoltage) + "V";
- return false;
- }
- faultMessage = "";
- return true;
- }
- void activateFailSafe() {
- criticalFault = true;
- systemEnabled = false;
- // Shut down all outputs
- for (int i = 0; i < 4; i++) {
- digitalWrite(PIN_INJECTOR_1 + i, LOW);
- digitalWrite(PIN_IGNITION_1 + i, LOW);
- }
- fuelPumpRunning = false;
- Serial.println("FAIL-SAFE ACTIVATED: " + faultMessage);
- }
- // ==================== WEB SERVER SETUP ====================
- void setupWebServer() {
- // Serve main page
- server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
- request->send_P(200, "text/html", index_html);
- });
- // Sensor data API
- server.on("/sensorData", HTTP_GET, [](AsyncWebServerRequest *request) {
- StaticJsonDocument<2048> doc;
- doc["rpm"] = currentRPM;
- doc["o2"] = o2Value;
- doc["tps"] = tpsValue;
- doc["map"] = mapValue;
- doc["temp"] = engineTemp;
- doc["voltage"] = batteryVoltage;
- doc["activeCylinder"] = currentCylinder;
- JsonArray pulseWidths = doc.createNestedArray("pulseWidths");
- for (int i = 0; i < 4; i++) {
- pulseWidths.add(currentPulseWidth[i]);
- }
- // Add ignition timing data
- JsonArray ignitionTiming = doc.createNestedArray("ignitionTiming");
- float ignitionAdvance = getIgnitionAdvance(currentRPM, mapValue);
- for (int i = 0; i < 4; i++) {
- ignitionTiming.add(ignitionAdvance);
- }
- doc["systemEnabled"] = systemEnabled;
- doc["fuelPumpRunning"] = fuelPumpRunning;
- doc["criticalFault"] = criticalFault;
- doc["faultMessage"] = faultMessage;
- doc["fuelMultiplier"] = tuning.fuelMultiplier;
- doc["ignitionAdvance"] = tuning.ignitionAdvance;
- doc["targetAFR"] = tuning.targetAFR;
- doc["rpmLimit"] = tuning.rpmLimit;
- doc["currentTune"] = tuning.currentTune;
- doc["fuelType"] = tuning.fuelType;
- // Lighting states
- doc["headlightState"] = headlightState;
- doc["highbeamState"] = highbeamState;
- doc["leftTurnState"] = leftTurnState;
- doc["rightTurnState"] = rightTurnState;
- doc["brakeLightState"] = brakeLightState;
- doc["hazardState"] = hazardState;
- String response;
- serializeJson(doc, response);
- request->send(200, "application/json", response);
- });
- // System control
- server.on("/system/toggle", HTTP_GET, [](AsyncWebServerRequest *request) {
- if (!criticalFault) {
- systemEnabled = !systemEnabled;
- if (systemEnabled) engineRunTime = millis();
- }
- request->send(200, "text/plain", "OK");
- });
- server.on("/pump/toggle", HTTP_GET, [](AsyncWebServerRequest *request) {
- if (!criticalFault) {
- fuelPumpRunning = !fuelPumpRunning;
- }
- request->send(200, "text/plain", "OK");
- });
- // Preset tunes
- server.on("/preset/economy", HTTP_GET, [](AsyncWebServerRequest *request) {
- tuning.fuelMultiplier = 0.9;
- tuning.ignitionAdvance = 14.0;
- tuning.targetAFR = 15.5;
- tuning.rpmLimit = 6500;
- tuning.currentTune = "economy";
- request->send(200, "text/plain", "OK");
- });
- server.on("/preset/performance", HTTP_GET, [](AsyncWebServerRequest *request) {
- tuning.fuelMultiplier = 1.0;
- tuning.ignitionAdvance = 12.0;
- tuning.targetAFR = 14.7;
- tuning.rpmLimit = 8000;
- tuning.currentTune = "performance";
- request->send(200, "text/plain", "OK");
- });
- server.on("/preset/race", HTTP_GET, [](AsyncWebServerRequest *request) {
- tuning.fuelMultiplier = 1.15;
- tuning.ignitionAdvance = 10.0;
- tuning.targetAFR = 13.5;
- tuning.rpmLimit = 9000;
- tuning.currentTune = "race";
- request->send(200, "text/plain", "OK");
- });
- server.on("/preset/methanol", HTTP_GET, [](AsyncWebServerRequest *request) {
- tuning.fuelMultiplier = 1.4;
- tuning.ignitionAdvance = 16.0;
- tuning.targetAFR = 6.5;
- tuning.rpmLimit = 8500;
- tuning.fuelType = "methanol";
- tuning.currentTune = "methanol";
- request->send(200, "text/plain", "OK");
- });
- server.on("/preset/gasoline", HTTP_GET, [](AsyncWebServerRequest *request) {
- tuning.fuelMultiplier = 1.0;
- tuning.ignitionAdvance = 12.0;
- tuning.targetAFR = 14.7;
- tuning.rpmLimit = 8000;
- tuning.fuelType = "gasoline";
- tuning.currentTune = "gasoline";
- request->send(200, "text/plain", "OK");
- });
- // Lighting control routes
- server.on("/lights/headlight/toggle", HTTP_GET, [](AsyncWebServerRequest *request) {
- toggleHeadlight();
- request->send(200, "text/plain", "OK");
- });
- server.on("/lights/highbeam/toggle", HTTP_GET, [](AsyncWebServerRequest *request) {
- toggleHighbeam();
- request->send(200, "text/plain", "OK");
- });
- server.on("/lights/leftturn/toggle", HTTP_GET, [](AsyncWebServerRequest *request) {
- toggleLeftTurn();
- request->send(200, "text/plain", "OK");
- });
- server.on("/lights/rightturn/toggle", HTTP_GET, [](AsyncWebServerRequest *request) {
- toggleRightTurn();
- request->send(200, "text/plain", "OK");
- });
- server.on("/lights/brake/toggle", HTTP_GET, [](AsyncWebServerRequest *request) {
- toggleBrakeLight();
- request->send(200, "text/plain", "OK");
- });
- server.on("/lights/hazard/toggle", HTTP_GET, [](AsyncWebServerRequest *request) {
- toggleHazard();
- request->send(200, "text/plain", "OK");
- });
- // Tuning parameter routes
- server.on("/tuning/fuelMultiplier", HTTP_GET, [](AsyncWebServerRequest *request) {
- if (request->hasParam("value")) {
- tuning.fuelMultiplier = request->getParam("value")->value().toFloat();
- }
- request->send(200, "text/plain", "OK");
- });
- server.on("/tuning/ignitionAdvance", HTTP_GET, [](AsyncWebServerRequest *request) {
- if (request->hasParam("value")) {
- tuning.ignitionAdvance = request->getParam("value")->value().toFloat();
- }
- request->send(200, "text/plain", "OK");
- });
- server.on("/tuning/targetAFR", HTTP_GET, [](AsyncWebServerRequest *request) {
- if (request->hasParam("value")) {
- tuning.targetAFR = request->getParam("value")->value().toFloat();
- }
- request->send(200, "text/plain", "OK");
- });
- server.on("/tuning/rpmLimit", HTTP_GET, [](AsyncWebServerRequest *request) {
- if (request->hasParam("value")) {
- tuning.rpmLimit = request->getParam("value")->value().toInt();
- tuning.maxRPM = tuning.rpmLimit;
- initializeFuelMap();
- initializeIgnitionMap();
- }
- request->send(200, "text/plain", "OK");
- });
- server.on("/tuning/save", HTTP_GET, [](AsyncWebServerRequest *request) {
- saveTuningToEEPROM();
- request->send(200, "text/plain", "OK");
- });
- server.on("/tuning/load", HTTP_GET, [](AsyncWebServerRequest *request) {
- loadTuningFromEEPROM();
- request->send(200, "text/plain", "OK");
- });
- server.on("/tuning/reset", HTTP_GET, [](AsyncWebServerRequest *request) {
- tuning.fuelMultiplier = 1.0;
- tuning.ignitionAdvance = 12.0;
- tuning.targetAFR = 14.7;
- tuning.rpmLimit = 8000;
- tuning.tempLimit = 105;
- tuning.minRPM = 0;
- tuning.maxRPM = 8000;
- tuning.rpmStep = 250;
- tuning.loadStep = 10;
- tuning.currentTune = "performance";
- tuning.fuelType = "gasoline";
- initializeFuelMap();
- initializeIgnitionMap();
- request->send(200, "text/plain", "OK");
- });
- // Fuel map API
- server.on("/fuelMap", HTTP_GET, [](AsyncWebServerRequest *request) {
- StaticJsonDocument<16384> doc;
- doc["name"] = fuelMap.name;
- doc["description"] = fuelMap.description;
- doc["fuelType"] = fuelMap.fuelType;
- doc["rpmLimit"] = tuning.rpmLimit;
- doc["rpmStep"] = tuning.rpmStep;
- doc["loadStep"] = tuning.loadStep;
- JsonArray rpmPoints = doc.createNestedArray("rpmPoints");
- for (int i = 0; i < fuelMap.rpmPointCount; i++) {
- rpmPoints.add(fuelMap.rpmPoints[i]);
- }
- JsonArray loadPoints = doc.createNestedArray("loadPoints");
- for (int i = 0; i < fuelMap.loadPointCount; i++) {
- loadPoints.add(fuelMap.loadPoints[i]);
- }
- JsonObject cells = doc.createNestedObject("cells");
- for (int i = 0; i < fuelMap.rpmPointCount; i++) {
- JsonObject rpmCell = cells.createNestedObject(String(fuelMap.rpmPoints[i]));
- for (int j = 0; j < fuelMap.loadPointCount; j++) {
- rpmCell[String(fuelMap.loadPoints[j])] = fuelMap.cells[i][j].pulseWidth;
- }
- }
- String response;
- serializeJson(doc, response);
- request->send(200, "application/json", response);
- });
- // Ignition map API
- server.on("/ignitionMap", HTTP_GET, [](AsyncWebServerRequest *request) {
- StaticJsonDocument<16384> doc;
- doc["name"] = ignitionMap.name;
- doc["description"] = ignitionMap.description;
- doc["rpmLimit"] = tuning.rpmLimit;
- doc["rpmStep"] = tuning.rpmStep;
- doc["loadStep"] = tuning.loadStep;
- JsonArray rpmPoints = doc.createNestedArray("rpmPoints");
- for (int i = 0; i < ignitionMap.rpmPointCount; i++) {
- rpmPoints.add(ignitionMap.rpmPoints[i]);
- }
- JsonArray loadPoints = doc.createNestedArray("loadPoints");
- for (int i = 0; i < ignitionMap.loadPointCount; i++) {
- loadPoints.add(ignitionMap.loadPoints[i]);
- }
- JsonObject cells = doc.createNestedObject("cells");
- for (int i = 0; i < ignitionMap.rpmPointCount; i++) {
- JsonObject rpmCell = cells.createNestedObject(String(ignitionMap.rpmPoints[i]));
- for (int j = 0; j < ignitionMap.loadPointCount; j++) {
- rpmCell[String(ignitionMap.loadPoints[j])] = ignitionMap.cells[i][j].advance;
- }
- }
- String response;
- serializeJson(doc, response);
- request->send(200, "application/json", response);
- });
- server.on("/fuelMap/update", HTTP_POST, [](AsyncWebServerRequest *request) {
- if (request->contentType() == "application/json") {
- String body = request->arg("plain");
- StaticJsonDocument<8192> doc;
- deserializeJson(doc, body);
- JsonArray updates = doc["updates"];
- for(JsonObject update : updates) {
- int rpm = update["rpm"];
- int load = update["load"];
- int pulseWidth = update["pulseWidth"];
- int rpmIndex = findRPMIndex(rpm, true);
- int loadIndex = findLoadIndex(load, true);
- if (rpmIndex >= 0 && rpmIndex < fuelMap.rpmPointCount &&
- loadIndex >= 0 && loadIndex < fuelMap.loadPointCount) {
- fuelMap.cells[rpmIndex][loadIndex].pulseWidth = pulseWidth;
- }
- }
- saveTuningToEEPROM();
- request->send(200, "text/plain", "OK");
- } else {
- request->send(400, "text/plain", "Invalid content type");
- }
- });
- server.on("/ignitionMap/update", HTTP_POST, [](AsyncWebServerRequest *request) {
- if (request->contentType() == "application/json") {
- String body = request->arg("plain");
- StaticJsonDocument<8192> doc;
- deserializeJson(doc, body);
- JsonArray updates = doc["updates"];
- for(JsonObject update : updates) {
- int rpm = update["rpm"];
- int load = update["load"];
- float advance = update["advance"];
- int rpmIndex = findRPMIndex(rpm, false);
- int loadIndex = findLoadIndex(load, false);
- if (rpmIndex >= 0 && rpmIndex < ignitionMap.rpmPointCount &&
- loadIndex >= 0 && loadIndex < ignitionMap.loadPointCount) {
- ignitionMap.cells[rpmIndex][loadIndex].advance = advance;
- }
- }
- saveTuningToEEPROM();
- request->send(200, "text/plain", "OK");
- } else {
- request->send(400, "text/plain", "Invalid content type");
- }
- });
- // Fuel map management
- server.on("/autotune/fuel", HTTP_GET, [](AsyncWebServerRequest *request) {
- float afrError = o2Value - tuning.targetAFR;
- if (abs(afrError) > 0.5) {
- float correction = 1.0 + (afrError * 0.1);
- for (int i = 0; i < fuelMap.rpmPointCount; i++) {
- for (int j = 0; j < fuelMap.loadPointCount; j++) {
- fuelMap.cells[i][j].pulseWidth *= correction;
- fuelMap.cells[i][j].learned = true;
- fuelMap.cells[i][j].correction = correction;
- }
- }
- saveTuningToEEPROM();
- }
- request->send(200, "text/plain", "Fuel auto-tune completed");
- });
- // Ignition map management
- server.on("/autotune/ignition", HTTP_GET, [](AsyncWebServerRequest *request) {
- float knockCorrection = 1.0;
- // Simulate knock detection at high load/RPM
- if (currentRPM > 6000 && mapValue > 80) {
- knockCorrection = 0.95;
- }
- for (int i = 0; i < ignitionMap.rpmPointCount; i++) {
- for (int j = 0; j < ignitionMap.loadPointCount; j++) {
- ignitionMap.cells[i][j].advance *= knockCorrection;
- ignitionMap.cells[i][j].learned = true;
- ignitionMap.cells[i][j].correction = knockCorrection;
- }
- }
- saveTuningToEEPROM();
- request->send(200, "text/plain", "Ignition auto-tune completed");
- });
- server.on("/smoothMap/fuel", HTTP_GET, [](AsyncWebServerRequest *request) {
- for (int i = 1; i < fuelMap.rpmPointCount - 1; i++) {
- for (int j = 1; j < fuelMap.loadPointCount - 1; j++) {
- int avg = (fuelMap.cells[i-1][j].pulseWidth +
- fuelMap.cells[i][j-1].pulseWidth +
- fuelMap.cells[i][j].pulseWidth +
- fuelMap.cells[i][j+1].pulseWidth +
- fuelMap.cells[i+1][j].pulseWidth) / 5;
- fuelMap.cells[i][j].pulseWidth = avg;
- }
- }
- saveTuningToEEPROM();
- request->send(200, "text/plain", "Fuel map smoothed");
- });
- server.on("/smoothMap/ignition", HTTP_GET, [](AsyncWebServerRequest *request) {
- for (int i = 1; i < ignitionMap.rpmPointCount - 1; i++) {
- for (int j = 1; j < ignitionMap.loadPointCount - 1; j++) {
- float avg = (ignitionMap.cells[i-1][j].advance +
- ignitionMap.cells[i][j-1].advance +
- ignitionMap.cells[i][j].advance +
- ignitionMap.cells[i][j+1].advance +
- ignitionMap.cells[i+1][j].advance) / 5.0;
- ignitionMap.cells[i][j].advance = avg;
- }
- }
- saveTuningToEEPROM();
- request->send(200, "text/plain", "Ignition map smoothed");
- });
- server.on("/resetMap/fuel", HTTP_GET, [](AsyncWebServerRequest *request) {
- initializeFuelMap();
- saveTuningToEEPROM();
- request->send(200, "text/plain", "Fuel map reset");
- });
- server.on("/resetMap/ignition", HTTP_GET, [](AsyncWebServerRequest *request) {
- initializeIgnitionMap();
- saveTuningToEEPROM();
- request->send(200, "text/plain", "Ignition map reset");
- });
- server.on("/flashMap/fuel", HTTP_GET, [](AsyncWebServerRequest *request) {
- isFlashing = true;
- flashStartTime = millis();
- saveTuningToEEPROM();
- request->send(200, "text/plain", "Fuel map flashed to ECU");
- });
- server.on("/flashMap/ignition", HTTP_GET, [](AsyncWebServerRequest *request) {
- isFlashing = true;
- flashStartTime = millis();
- saveTuningToEEPROM();
- request->send(200, "text/plain", "Ignition map flashed to ECU");
- });
- server.on("/addRPM/fuel", HTTP_POST, [](AsyncWebServerRequest *request) {
- if (request->contentType() == "application/json") {
- String body = request->arg("plain");
- StaticJsonDocument<256> doc;
- deserializeJson(doc, body);
- int newRPM = doc["rpm"];
- if (fuelMap.rpmPointCount < MAX_RPM_POINTS) {
- fuelMap.rpmPoints[fuelMap.rpmPointCount] = newRPM;
- fuelMap.rpmPointCount++;
- // Simple bubble sort
- for (int i = 0; i < fuelMap.rpmPointCount - 1; i++) {
- for (int j = 0; j < fuelMap.rpmPointCount - i - 1; j++) {
- if (fuelMap.rpmPoints[j] > fuelMap.rpmPoints[j + 1]) {
- int temp = fuelMap.rpmPoints[j];
- fuelMap.rpmPoints[j] = fuelMap.rpmPoints[j + 1];
- fuelMap.rpmPoints[j + 1] = temp;
- }
- }
- }
- int newIndex = findRPMIndex(newRPM, true);
- for (int j = 0; j < fuelMap.loadPointCount; j++) {
- if (newIndex > 0 && newIndex < fuelMap.rpmPointCount - 1) {
- int prevRPM = fuelMap.rpmPoints[newIndex - 1];
- int nextRPM = fuelMap.rpmPoints[newIndex + 1];
- int prevPulse = fuelMap.cells[newIndex - 1][j].pulseWidth;
- int nextPulse = fuelMap.cells[newIndex + 1][j].pulseWidth;
- float ratio = (float)(newRPM - prevRPM) / (nextRPM - prevRPM);
- fuelMap.cells[newIndex][j].pulseWidth = prevPulse + (nextPulse - prevPulse) * ratio;
- } else {
- fuelMap.cells[newIndex][j].pulseWidth = 800 + (newRPM * 0.1) + (fuelMap.loadPoints[j] * 8);
- }
- }
- saveTuningToEEPROM();
- request->send(200, "text/plain", "Fuel RPM point added");
- } else {
- request->send(507, "text/plain", "Maximum fuel RPM points reached");
- }
- } else {
- request->send(400, "text/plain", "Invalid content type");
- }
- });
- server.on("/addRPM/ignition", HTTP_POST, [](AsyncWebServerRequest *request) {
- if (request->contentType() == "application/json") {
- String body = request->arg("plain");
- StaticJsonDocument<256> doc;
- deserializeJson(doc, body);
- int newRPM = doc["rpm"];
- if (ignitionMap.rpmPointCount < MAX_RPM_POINTS) {
- ignitionMap.rpmPoints[ignitionMap.rpmPointCount] = newRPM;
- ignitionMap.rpmPointCount++;
- // Simple bubble sort
- for (int i = 0; i < ignitionMap.rpmPointCount - 1; i++) {
- for (int j = 0; j < ignitionMap.rpmPointCount - i - 1; j++) {
- if (ignitionMap.rpmPoints[j] > ignitionMap.rpmPoints[j + 1]) {
- int temp = ignitionMap.rpmPoints[j];
- ignitionMap.rpmPoints[j] = ignitionMap.rpmPoints[j + 1];
- ignitionMap.rpmPoints[j + 1] = temp;
- }
- }
- }
- int newIndex = findRPMIndex(newRPM, false);
- for (int j = 0; j < ignitionMap.loadPointCount; j++) {
- if (newIndex > 0 && newIndex < ignitionMap.rpmPointCount - 1) {
- int prevRPM = ignitionMap.rpmPoints[newIndex - 1];
- int nextRPM = ignitionMap.rpmPoints[newIndex + 1];
- float prevAdvance = ignitionMap.cells[newIndex - 1][j].advance;
- float nextAdvance = ignitionMap.cells[newIndex + 1][j].advance;
- float ratio = (float)(newRPM - prevRPM) / (nextRPM - prevRPM);
- ignitionMap.cells[newIndex][j].advance = prevAdvance + (nextAdvance - prevAdvance) * ratio;
- } else {
- ignitionMap.cells[newIndex][j].advance = 10.0 + (newRPM * 0.001) + (ignitionMap.loadPoints[j] * 0.1);
- }
- }
- saveTuningToEEPROM();
- request->send(200, "text/plain", "Ignition RPM point added");
- } else {
- request->send(507, "text/plain", "Maximum ignition RPM points reached");
- }
- } else {
- request->send(400, "text/plain", "Invalid content type");
- }
- });
- server.on("/removeRPM/fuel", HTTP_POST, [](AsyncWebServerRequest *request) {
- if (request->contentType() == "application/json") {
- String body = request->arg("plain");
- StaticJsonDocument<256> doc;
- deserializeJson(doc, body);
- int removeRPM = doc["rpm"];
- int removeIndex = -1;
- for (int i = 0; i < fuelMap.rpmPointCount; i++) {
- if (fuelMap.rpmPoints[i] == removeRPM) {
- removeIndex = i;
- break;
- }
- }
- if (removeIndex != -1 && fuelMap.rpmPointCount > 1) {
- for (int i = removeIndex; i < fuelMap.rpmPointCount - 1; i++) {
- fuelMap.rpmPoints[i] = fuelMap.rpmPoints[i + 1];
- for (int j = 0; j < fuelMap.loadPointCount; j++) {
- fuelMap.cells[i][j] = fuelMap.cells[i + 1][j];
- }
- }
- fuelMap.rpmPointCount--;
- saveTuningToEEPROM();
- request->send(200, "text/plain", "Fuel RPM point removed");
- } else {
- request->send(400, "text/plain", "Fuel RPM point not found or cannot remove last point");
- }
- } else {
- request->send(400, "text/plain", "Invalid content type");
- }
- });
- server.on("/removeRPM/ignition", HTTP_POST, [](AsyncWebServerRequest *request) {
- if (request->contentType() == "application/json") {
- String body = request->arg("plain");
- StaticJsonDocument<256> doc;
- deserializeJson(doc, body);
- int removeRPM = doc["rpm"];
- int removeIndex = -1;
- for (int i = 0; i < ignitionMap.rpmPointCount; i++) {
- if (ignitionMap.rpmPoints[i] == removeRPM) {
- removeIndex = i;
- break;
- }
- }
- if (removeIndex != -1 && ignitionMap.rpmPointCount > 1) {
- for (int i = removeIndex; i < ignitionMap.rpmPointCount - 1; i++) {
- ignitionMap.rpmPoints[i] = ignitionMap.rpmPoints[i + 1];
- for (int j = 0; j < ignitionMap.loadPointCount; j++) {
- ignitionMap.cells[i][j] = ignitionMap.cells[i + 1][j];
- }
- }
- ignitionMap.rpmPointCount--;
- saveTuningToEEPROM();
- request->send(200, "text/plain", "Ignition RPM point removed");
- } else {
- request->send(400, "text/plain", "Ignition RPM point not found or cannot remove last point");
- }
- } else {
- request->send(400, "text/plain", "Invalid content type");
- }
- });
- server.begin();
- }
- // ==================== SETUP ====================
- void setup() {
- Serial.begin(115200);
- // Initialize output pins
- pinMode(PIN_INJECTOR_1, OUTPUT);
- pinMode(PIN_INJECTOR_2, OUTPUT);
- pinMode(PIN_INJECTOR_3, OUTPUT);
- pinMode(PIN_INJECTOR_4, OUTPUT);
- digitalWrite(PIN_INJECTOR_1, LOW);
- digitalWrite(PIN_INJECTOR_2, LOW);
- digitalWrite(PIN_INJECTOR_3, LOW);
- digitalWrite(PIN_INJECTOR_4, LOW);
- pinMode(PIN_IGNITION_1, OUTPUT);
- pinMode(PIN_IGNITION_2, OUTPUT);
- pinMode(PIN_IGNITION_3, OUTPUT);
- pinMode(PIN_IGNITION_4, OUTPUT);
- digitalWrite(PIN_IGNITION_1, LOW);
- digitalWrite(PIN_IGNITION_2, LOW);
- digitalWrite(PIN_IGNITION_3, LOW);
- digitalWrite(PIN_IGNITION_4, LOW);
- pinMode(PIN_HEADLIGHT, OUTPUT);
- pinMode(PIN_HIGHBEAM, OUTPUT);
- pinMode(PIN_LEFT_TURN, OUTPUT);
- pinMode(PIN_RIGHT_TURN, OUTPUT);
- pinMode(PIN_BRAKE_LIGHT, OUTPUT);
- digitalWrite(PIN_HEADLIGHT, LOW);
- digitalWrite(PIN_HIGHBEAM, LOW);
- digitalWrite(PIN_LEFT_TURN, LOW);
- digitalWrite(PIN_RIGHT_TURN, LOW);
- digitalWrite(PIN_BRAKE_LIGHT, LOW);
- pinMode(PIN_O2_SENSOR, INPUT);
- pinMode(PIN_TPS, INPUT);
- pinMode(PIN_MAP_SENSOR, INPUT);
- pinMode(PIN_ENGINE_TEMP, INPUT);
- pinMode(PIN_RPM, INPUT_PULLUP);
- attachInterrupt(digitalPinToInterrupt(PIN_RPM), rpmInterrupt, RISING);
- initializeFuelMap();
- initializeIgnitionMap();
- loadTuningFromEEPROM();
- WiFi.softAP(ssid, password);
- setupWebServer();
- Serial.println("Professional 4-Cylinder ECU Started");
- Serial.print("IP Address: ");
- Serial.println(WiFi.softAPIP());
- Serial.println("Connect to WiFi: ESP32_ECU");
- Serial.println("Password: tune12345");
- Serial.println("Open browser to: http://192.168.4.1");
- }
- // ==================== MAIN LOOP ====================
- void loop() {
- readSensors();
- updateLighting();
- if (isFlashing && (millis() - flashStartTime > 2000)) {
- isFlashing = false;
- }
- if (!performSafetyChecks()) {
- if (!criticalFault) {
- activateFailSafe();
- }
- } else {
- criticalFault = false;
- }
- if (!criticalFault && systemEnabled) {
- controlInjection();
- controlIgnition();
- } else {
- for (int i = 0; i < 4; i++) {
- digitalWrite(PIN_INJECTOR_1 + i, LOW);
- digitalWrite(PIN_IGNITION_1 + i, LOW);
- }
- }
- delay(10);
- }
Advertisement
Add Comment
Please, Sign In to add comment