frayray909090

ecu_firmware

Nov 11th, 2025
282
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C++ 124.77 KB | None | 0 0
  1. /*******************************************************
  2.  * 4-CYLINDER ESP32 ECU SYSTEM
  3.  *
  4.  * Professional Features:
  5.  * - 4 injectors and 4 ignition channels
  6.  * - Dynamic fuel AND ignition maps with customizable RPM range
  7.  * - Professional web-based tuning interface with dual map editors
  8.  * - Complete lighting controls (headlights, turns, brakes)
  9.  * - Real-time sensor monitoring and fail-safes
  10.  * - Advanced fuel & ignition map editors with 0-RPM to limit range
  11.  * - Multiple fuel & ignition map support with selection interface
  12.  * - WORKING 3D visualization and graph views for both maps
  13.  *******************************************************/
  14.  
  15. #include <WiFi.h>
  16. #include <AsyncTCP.h>
  17. #include <ESPAsyncWebServer.h>
  18. #include <ArduinoJson.h>
  19. #include <EEPROM.h>
  20.  
  21. // ==================== CONFIGURATION ====================
  22. const char* ssid = "ESP32_ECU";
  23. const char* password = "tune12345";
  24.  
  25. // ==================== PIN DEFINITIONS ====================
  26. // Fuel Injectors
  27. #define PIN_INJECTOR_1      12
  28. #define PIN_INJECTOR_2      14  
  29. #define PIN_INJECTOR_3      27
  30. #define PIN_INJECTOR_4      26
  31.  
  32. // Ignition Coils
  33. #define PIN_IGNITION_1      9
  34. #define PIN_IGNITION_2      13
  35. #define PIN_IGNITION_3      11
  36. #define PIN_IGNITION_4      10
  37.  
  38. // Lighting Outputs
  39. #define PIN_HEADLIGHT       17
  40. #define PIN_HIGHBEAM        16
  41. #define PIN_LEFT_TURN       5
  42. #define PIN_RIGHT_TURN      4
  43. #define PIN_BRAKE_LIGHT     8
  44.  
  45. // Sensor Inputs
  46. #define PIN_O2_SENSOR       32
  47. #define PIN_TPS             33
  48. #define PIN_MAP_SENSOR      34
  49. #define PIN_ENGINE_TEMP     35
  50. #define PIN_RPM             17
  51.  
  52. // ==================== GLOBAL VARIABLES ====================
  53. AsyncWebServer server(80);
  54.  
  55. // Engine State
  56. volatile unsigned long rpmPulseTime = 0;
  57. volatile unsigned long lastRpmPulse = 0;
  58. float currentRPM = 0;
  59. float o2Value = 14.7;
  60. float tpsValue = 0;
  61. float mapValue = 0;
  62. float engineTemp = 20;
  63. float batteryVoltage = 12.5;
  64.  
  65. // System State
  66. bool systemEnabled = false;
  67. bool fuelPumpRunning = false;
  68. bool criticalFault = false;
  69. String faultMessage = "";
  70.  
  71. // Injection Tracking
  72. unsigned long lastInjectorPulse[4] = {0, 0, 0, 0};
  73. int currentPulseWidth[4] = {0, 0, 0, 0};
  74. unsigned long engineRunTime = 0;
  75.  
  76. // Lighting State
  77. bool headlightState = false;
  78. bool highbeamState = false;
  79. bool leftTurnState = false;
  80. bool rightTurnState = false;
  81. bool brakeLightState = false;
  82. bool hazardState = false;
  83. unsigned long turnSignalPreviousMillis = 0;
  84. bool turnSignalBlinkState = false;
  85. const long turnSignalInterval = 500;
  86.  
  87. // Tuning Parameters
  88. struct TuningParams {
  89.   float fuelMultiplier = 1.0;
  90.   float ignitionAdvance = 12.0;
  91.   float targetAFR = 14.7;
  92.   int rpmLimit = 8000;
  93.   int tempLimit = 105;
  94.   int minRPM = 0;
  95.   int maxRPM = 8000;
  96.   int rpmStep = 250;
  97.   int loadStep = 10;
  98.   String currentTune = "performance";
  99.   String fuelType = "gasoline";
  100. };
  101. TuningParams tuning;
  102.  
  103. // Advanced Fuel & Ignition Map Structures
  104. #define MAX_RPM_POINTS 100  // Support up to 100 RPM points (0-10000 RPM)
  105. #define MAX_LOAD_POINTS 11  // 0-100% in 10% steps + 0%
  106.  
  107. struct FuelMapCell {
  108.   int pulseWidth;
  109.   bool learned;
  110.   float correction;
  111. };
  112.  
  113. struct IgnitionMapCell {
  114.   float advance;
  115.   bool learned;
  116.   float correction;
  117. };
  118.  
  119. struct FuelMap {
  120.   String name;
  121.   String description;
  122.   String fuelType;
  123.   int rpmPoints[MAX_RPM_POINTS];
  124.   int loadPoints[MAX_LOAD_POINTS];
  125.   FuelMapCell cells[MAX_RPM_POINTS][MAX_LOAD_POINTS];
  126.   int rpmPointCount;
  127.   int loadPointCount;
  128. };
  129.  
  130. struct IgnitionMap {
  131.   String name;
  132.   String description;
  133.   int rpmPoints[MAX_RPM_POINTS];
  134.   int loadPoints[MAX_LOAD_POINTS];
  135.   IgnitionMapCell cells[MAX_RPM_POINTS][MAX_LOAD_POINTS];
  136.   int rpmPointCount;
  137.   int loadPointCount;
  138. };
  139.  
  140. FuelMap fuelMap;
  141. IgnitionMap ignitionMap;
  142. bool isFlashing = false;
  143. unsigned long flashStartTime = 0;
  144.  
  145. // Multiple fuel & ignition maps storage
  146. #define MAX_FUEL_MAPS 10
  147. #define MAX_IGNITION_MAPS 10
  148. FuelMap fuelMaps[MAX_FUEL_MAPS];
  149. IgnitionMap ignitionMaps[MAX_IGNITION_MAPS];
  150. int currentFuelMapIndex = 0;
  151. int currentIgnitionMapIndex = 0;
  152. int fuelMapCount = 0;
  153. int ignitionMapCount = 0;
  154.  
  155. // Firing order (1-3-4-2 typical for inline-4)
  156. const int firingOrder[4] = {0, 2, 3, 1}; // Injector order
  157. const int ignitionOrder[4] = {0, 2, 3, 1}; // Ignition order
  158. int currentCylinder = 0;
  159. unsigned long lastCylinderTime = 0;
  160.  
  161. // ==================== EMBEDDED WEBPAGE ====================
  162. const char index_html[] PROGMEM = R"rawliteral(
  163. <!DOCTYPE HTML><html>
  164. <head>
  165.  <title>Professional 4-Cylinder ECU Tuner</title>
  166.  <meta name="viewport" content="width=device-width, initial-scale=1">
  167.  <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
  168.   <script src="https://cdnjs.cloudflare.com/ajax/libs/chart.js/3.7.0/chart.min.js"></script>
  169.   <style>
  170.     :root {
  171.       --primary: #2c3e50;
  172.       --secondary: #3498db;
  173.       --success: #27ae60;
  174.       --danger: #e74c3c;
  175.       --warning: #f39c12;
  176.       --info: #17a2b8;
  177.       --dark: #34495e;
  178.       --light: #ecf0f1;
  179.       --fuel-color: #e74c3c;
  180.       --ignition-color: #3498db;
  181.     }
  182.     * {
  183.       box-sizing: border-box;
  184.       margin: 0;
  185.       padding: 0;
  186.     }
  187.     body {
  188.       font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  189.       background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  190.       color: #333;
  191.       min-height: 100vh;
  192.       padding: 20px;
  193.     }
  194.     .container {
  195.       max-width: 1600px;
  196.       margin: 0 auto;
  197.     }
  198.     .header {
  199.       background: white;
  200.       padding: 20px;
  201.       border-radius: 15px;
  202.       box-shadow: 0 10px 30px rgba(0,0,0,0.2);
  203.       margin-bottom: 20px;
  204.       text-align: center;
  205.     }
  206.     .header h1 {
  207.       color: var(--primary);
  208.       margin-bottom: 10px;
  209.     }
  210.     .grid {
  211.       display: grid;
  212.       grid-template-columns: 1fr 1fr;
  213.       gap: 20px;
  214.       margin-bottom: 20px;
  215.     }
  216.     @media (max-width: 768px) {
  217.       .grid {
  218.         grid-template-columns: 1fr;
  219.       }
  220.     }
  221.     .card {
  222.       background: white;
  223.       padding: 25px;
  224.       border-radius: 15px;
  225.       box-shadow: 0 5px 15px rgba(0,0,0,0.1);
  226.     }
  227.     .card h2 {
  228.       color: var(--primary);
  229.       margin-bottom: 20px;
  230.       padding-bottom: 10px;
  231.       border-bottom: 2px solid var(--secondary);
  232.     }
  233.    
  234.     /* Map Editor Tabs */
  235.     .map-tabs {
  236.       display: flex;
  237.       background: var(--light);
  238.       border-radius: 8px;
  239.       padding: 5px;
  240.       margin-bottom: 20px;
  241.     }
  242.     .map-tab {
  243.       padding: 12px 20px;
  244.       border: none;
  245.       background: transparent;
  246.       border-radius: 5px;
  247.       cursor: pointer;
  248.       font-weight: 600;
  249.       transition: all 0.3s ease;
  250.       flex: 1;
  251.       text-align: center;
  252.     }
  253.     .map-tab.active {
  254.       background: var(--secondary);
  255.       color: white;
  256.     }
  257.     .map-tab.fuel {
  258.       border-bottom: 3px solid var(--fuel-color);
  259.     }
  260.     .map-tab.ignition {
  261.       border-bottom: 3px solid var(--ignition-color);
  262.     }
  263.    
  264.     /* Sensor Grid */
  265.     .sensor-grid {
  266.       display: grid;
  267.       grid-template-columns: repeat(3, 1fr);
  268.       gap: 15px;
  269.     }
  270.     .sensor-item {
  271.       text-align: center;
  272.       padding: 15px;
  273.       background: var(--light);
  274.       border-radius: 10px;
  275.     }
  276.     .sensor-value {
  277.       font-size: 1.8em;
  278.       font-weight: bold;
  279.       color: var(--primary);
  280.       margin: 5px 0;
  281.     }
  282.     .sensor-unit {
  283.       color: #7f8c8d;
  284.       font-size: 0.9em;
  285.     }
  286.     .gauge {
  287.       width: 100%;
  288.       height: 20px;
  289.       background: var(--light);
  290.       border-radius: 10px;
  291.       overflow: hidden;
  292.       margin: 10px 0;
  293.     }
  294.     .gauge-fill {
  295.       height: 100%;
  296.       background: linear-gradient(90deg, var(--success), var(--warning), var(--danger));
  297.       transition: width 0.5s ease;
  298.     }
  299.    
  300.     /* Buttons and Controls */
  301.     .btn {
  302.       padding: 12px 20px;
  303.       margin: 5px;
  304.       border: none;
  305.       border-radius: 8px;
  306.       cursor: pointer;
  307.       font-size: 14px;
  308.       font-weight: 600;
  309.       transition: all 0.3s ease;
  310.       min-width: 110px;
  311.     }
  312.     .btn:hover {
  313.       transform: translateY(-2px);
  314.       box-shadow: 0 5px 15px rgba(0,0,0,0.2);
  315.     }
  316.     .btn-primary { background: var(--secondary); color: white; }
  317.     .btn-success { background: var(--success); color: white; }
  318.     .btn-danger { background: var(--danger); color: white; }
  319.     .btn-warning { background: var(--warning); color: white; }
  320.     .btn-info { background: var(--info); color: white; }
  321.     .btn-fuel { background: var(--fuel-color); color: white; }
  322.     .btn-ignition { background: var(--ignition-color); color: white; }
  323.     .btn-active {
  324.       background: var(--dark) !important;
  325.       color: white;
  326.       box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.3);
  327.     }
  328.    
  329.     .control-group {
  330.       display: flex;
  331.       flex-wrap: wrap;
  332.       justify-content: center;
  333.       gap: 10px;
  334.       margin: 15px 0;
  335.     }
  336.     .preset-grid {
  337.       display: grid;
  338.       grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  339.       gap: 10px;
  340.       margin: 15px 0;
  341.     }
  342.    
  343.     /* Map Table */
  344.     .map-container {
  345.       overflow-x: auto;
  346.       margin: 20px 0;
  347.     }
  348.     .map-table {
  349.       width: 100%;
  350.       border-collapse: collapse;
  351.       font-size: 12px;
  352.       background: white;
  353.     }
  354.     .map-table th,
  355.     .map-table td {
  356.       padding: 8px;
  357.       text-align: center;
  358.       border: 1px solid #ddd;
  359.       min-width: 60px;
  360.     }
  361.     .map-table th {
  362.       background: var(--primary);
  363.       color: white;
  364.       position: sticky;
  365.       top: 0;
  366.     }
  367.     .map-table th:first-child {
  368.       position: sticky;
  369.       left: 0;
  370.       background: var(--dark);
  371.       z-index: 2;
  372.     }
  373.     .map-table td:first-child {
  374.       position: sticky;
  375.       left: 0;
  376.       background: var(--light);
  377.       font-weight: bold;
  378.       z-index: 1;
  379.     }
  380.     .map-table input {
  381.       width: 100%;
  382.       padding: 4px;
  383.       border: 1px solid #ddd;
  384.       border-radius: 3px;
  385.       text-align: center;
  386.       font-size: 11px;
  387.     }
  388.     .map-table input:focus {
  389.       border-color: var(--secondary);
  390.       outline: none;
  391.       background: #f0f8ff;
  392.     }
  393.    
  394.     /* Lighting Controls */
  395.     .lighting-grid {
  396.       display: grid;
  397.       grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  398.       gap: 15px;
  399.       margin: 15px 0;
  400.     }
  401.     .light-item {
  402.       background: var(--light);
  403.       padding: 15px;
  404.       border-radius: 10px;
  405.       text-align: center;
  406.     }
  407.     .light-status {
  408.       font-size: 1.2em;
  409.       font-weight: bold;
  410.       margin: 10px 0;
  411.     }
  412.     .status-on { color: var(--success); }
  413.     .status-off { color: var(--danger); }
  414.     .status-blinking { color: var(--warning); animation: blink 1s infinite; }
  415.     @keyframes blink { 50% { opacity: 0.5; } }
  416.    
  417.     /* Tuning Controls */
  418.     .slider-container {
  419.       margin: 15px 0;
  420.     }
  421.     .slider-container label {
  422.       display: block;
  423.       margin-bottom: 8px;
  424.       font-weight: 600;
  425.       color: var(--primary);
  426.     }
  427.     .slider-value {
  428.       float: right;
  429.       font-weight: bold;
  430.       color: var(--secondary);
  431.     }
  432.     input[type="range"] {
  433.       width: 100%;
  434.       height: 8px;
  435.       border-radius: 4px;
  436.       background: var(--light);
  437.       outline: none;
  438.     }
  439.    
  440.     .status-indicator {
  441.       display: inline-block;
  442.       width: 12px;
  443.       height: 12px;
  444.       border-radius: 50%;
  445.       margin-right: 8px;
  446.     }
  447.     .status-on { background: var(--success); }
  448.     .status-off { background: var(--danger); }
  449.     .status-fault { background: var(--warning); animation: pulse 1s infinite; }
  450.     @keyframes pulse {
  451.       0% { opacity: 1; }
  452.       50% { opacity: 0.5; }
  453.       100% { opacity: 1; }
  454.     }
  455.    
  456.     .fault-panel {
  457.       background: var(--danger);
  458.       color: white;
  459.       padding: 15px;
  460.       border-radius: 10px;
  461.       margin: 10px 0;
  462.       text-align: center;
  463.       display: none;
  464.     }
  465.    
  466.     .map-controls {
  467.       display: flex;
  468.       justify-content: space-between;
  469.       align-items: center;
  470.       margin: 15px 0;
  471.       flex-wrap: wrap;
  472.       gap: 10px;
  473.     }
  474.    
  475.     /* Cylinder Info */
  476.     .cylinder-grid {
  477.       display: grid;
  478.       grid-template-columns: repeat(4, 1fr);
  479.       gap: 10px;
  480.       margin: 15px 0;
  481.     }
  482.     .cylinder-item {
  483.       background: var(--light);
  484.       padding: 15px;
  485.       border-radius: 10px;
  486.       text-align: center;
  487.     }
  488.     .cylinder-active {
  489.       background: #e8f4fd;
  490.       border: 2px solid var(--secondary);
  491.     }
  492.    
  493.     /* Tune Info */
  494.     .tune-info {
  495.       background: #e8f4fd;
  496.       padding: 15px;
  497.       border-radius: 10px;
  498.       margin: 10px 0;
  499.       border-left: 4px solid var(--secondary);
  500.     }
  501.    
  502.     /* Map Selection */
  503.     .map-selection-grid {
  504.       display: grid;
  505.       grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  506.       gap: 15px;
  507.       margin: 15px 0;
  508.     }
  509.     .map-item {
  510.       background: var(--light);
  511.       padding: 15px;
  512.       border-radius: 10px;
  513.       text-align: center;
  514.       cursor: pointer;
  515.       transition: all 0.3s ease;
  516.       border: 2px solid transparent;
  517.     }
  518.     .map-item:hover {
  519.       transform: translateY(-3px);
  520.       box-shadow: 0 5px 15px rgba(0,0,0,0.1);
  521.     }
  522.     .map-item.active {
  523.       border-color: var(--secondary);
  524.       background: #e8f4fd;
  525.     }
  526.     .map-item h3 {
  527.       color: var(--primary);
  528.       margin-bottom: 10px;
  529.     }
  530.     .map-meta {
  531.       font-size: 0.8em;
  532.       color: #7f8c8d;
  533.       margin: 5px 0;
  534.     }
  535.     .map-actions {
  536.       margin-top: 10px;
  537.       display: flex;
  538.       gap: 5px;
  539.       justify-content: center;
  540.     }
  541.     .map-actions .btn {
  542.       padding: 6px 12px;
  543.       font-size: 12px;
  544.       min-width: auto;
  545.     }
  546.    
  547.     /* View Toggle */
  548.     .view-toggle {
  549.       display: flex;
  550.       background: var(--light);
  551.       border-radius: 8px;
  552.       padding: 5px;
  553.     }
  554.     .view-toggle button {
  555.       padding: 8px 15px;
  556.       border: none;
  557.       background: transparent;
  558.       border-radius: 5px;
  559.       cursor: pointer;
  560.     }
  561.     .view-toggle button.active {
  562.       background: var(--secondary);
  563.       color: white;
  564.     }
  565.    
  566.     /* 3D View */
  567.     .map-3d {
  568.       width: 100%;
  569.       height: 400px;
  570.       background: #1a1a1a;
  571.       border-radius: 10px;
  572.       margin: 15px 0;
  573.       position: relative;
  574.       overflow: hidden;
  575.     }
  576.     .map-3d canvas {
  577.       border-radius: 10px;
  578.     }
  579.    
  580.     /* Graph View */
  581.     .map-graph {
  582.       width: 100%;
  583.       height: 400px;
  584.       background: white;
  585.       border-radius: 10px;
  586.       margin: 15px 0;
  587.       position: relative;
  588.     }
  589.     .map-graph canvas {
  590.       border-radius: 10px;
  591.     }
  592.    
  593.     /* Map Editor Sections */
  594.     .map-editor-section {
  595.       display: none;
  596.     }
  597.     .map-editor-section.active {
  598.       display: block;
  599.     }
  600.    
  601.     /* Visualization Controls */
  602.     .viz-controls {
  603.       position: absolute;
  604.       top: 10px;
  605.       right: 10px;
  606.       background: rgba(0,0,0,0.7);
  607.       padding: 10px;
  608.       border-radius: 5px;
  609.       z-index: 10;
  610.     }
  611.     .viz-controls button {
  612.       background: #444;
  613.       color: white;
  614.       border: none;
  615.       padding: 5px 10px;
  616.       margin: 2px;
  617.       border-radius: 3px;
  618.       cursor: pointer;
  619.     }
  620.     .viz-controls button:hover {
  621.       background: #666;
  622.     }
  623.   </style>
  624. </head>
  625. <body>
  626.   <div class="container">
  627.     <div class="header">
  628.       <h1>Professional 4-Cylinder ECU Tuner</h1>
  629.       <p>Advanced Engine Management & Lighting Control</p>
  630.     </div>
  631.  
  632.     <div class="fault-panel" id="faultPanel">
  633.       <strong>CRITICAL FAULT:</strong> <span id="faultMessage"></span>
  634.     </div>
  635.  
  636.     <div class="grid">
  637.       <!-- Sensor Data Panel -->
  638.       <div class="card">
  639.         <h2>Live Engine Data</h2>
  640.         <div class="sensor-grid">
  641.           <div class="sensor-item">
  642.             <div>Engine RPM</div>
  643.             <div class="sensor-value" id="rpmValue">0</div>
  644.             <div class="sensor-unit">RPM</div>
  645.             <div class="gauge"><div class="gauge-fill" id="rpmGauge" style="width: 0%"></div></div>
  646.           </div>
  647.           <div class="sensor-item">
  648.             <div>Air/Fuel Ratio</div>
  649.             <div class="sensor-value" id="o2Value">0.00</div>
  650.             <div class="sensor-unit">AFR</div>
  651.             <div class="gauge"><div class="gauge-fill" id="o2Gauge" style="width: 0%"></div></div>
  652.           </div>
  653.           <div class="sensor-item">
  654.             <div>Throttle Position</div>
  655.             <div class="sensor-value" id="tpsValue">0%</div>
  656.             <div class="sensor-unit">Percentage</div>
  657.             <div class="gauge"><div class="gauge-fill" id="tpsGauge" style="width: 0%"></div></div>
  658.           </div>
  659.           <div class="sensor-item">
  660.             <div>Engine Load</div>
  661.             <div class="sensor-value" id="mapValue">0%</div>
  662.             <div class="sensor-unit">MAP</div>
  663.             <div class="gauge"><div class="gauge-fill" id="mapGauge" style="width: 0%"></div></div>
  664.           </div>
  665.           <div class="sensor-item">
  666.             <div>Engine Temp</div>
  667.             <div class="sensor-value" id="tempValue">0°C</div>
  668.             <div class="sensor-unit">Celsius</div>
  669.             <div class="gauge"><div class="gauge-fill" id="tempGauge" style="width: 0%"></div></div>
  670.           </div>
  671.           <div class="sensor-item">
  672.             <div>Battery Voltage</div>
  673.             <div class="sensor-value" id="voltageValue">0.0</div>
  674.             <div class="sensor-unit">Volts</div>
  675.             <div class="gauge"><div class="gauge-fill" id="voltageGauge" style="width: 0%"></div></div>
  676.           </div>
  677.         </div>
  678.        
  679.         <div style="margin-top: 20px; padding: 15px; background: #f8f9fa; border-radius: 10px;">
  680.           <div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap;">
  681.             <div>
  682.               <span class="status-indicator" id="systemStatusLight"></span>
  683.               System: <span id="systemStatusText">OFF</span>
  684.             </div>
  685.             <div>Active Cylinder: <span id="activeCylinder">-</span></div>
  686.             <div>Fuel: <span id="fuelType">gasoline</span></div>
  687.             <div>Tune: <span id="currentTune">performance</span></div>
  688.           </div>
  689.         </div>
  690.  
  691.         <!-- Cylinder Information -->
  692.         <h3>Cylinder Status</h3>
  693.         <div class="cylinder-grid">
  694.           <div class="cylinder-item" id="cyl1">
  695.             <strong>Cylinder 1</strong><br>
  696.             Pulse: <span id="pulse1">0</span>µs<br>
  697.             Timing: <span id="timing1">0</span>°
  698.           </div>
  699.           <div class="cylinder-item" id="cyl2">
  700.             <strong>Cylinder 2</strong><br>
  701.             Pulse: <span id="pulse2">0</span>µs<br>
  702.             Timing: <span id="timing2">0</span>°
  703.           </div>
  704.           <div class="cylinder-item" id="cyl3">
  705.             <strong>Cylinder 3</strong><br>
  706.             Pulse: <span id="pulse3">0</span>µs<br>
  707.             Timing: <span id="timing3">0</span>°
  708.           </div>
  709.           <div class="cylinder-item" id="cyl4">
  710.             <strong>Cylinder 4</strong><br>
  711.             Pulse: <span id="pulse4">0</span>µs<br>
  712.             Timing: <span id="timing4">0</span>°
  713.           </div>
  714.         </div>
  715.       </div>
  716.  
  717.       <!-- System Control Panel -->
  718.       <div class="card">
  719.         <h2>System Controls</h2>
  720.        
  721.         <div class="control-group">
  722.           <button class="btn btn-success" onclick="toggleSystem()">
  723.             <span class="status-indicator status-off" id="powerLight"></span>
  724.             Power: <span id="powerStatus">OFF</span>
  725.           </button>
  726.           <button class="btn btn-primary" onclick="toggleFuelPump()">
  727.             Fuel Pump: <span id="pumpStatus">OFF</span>
  728.           </button>
  729.         </div>
  730.  
  731.         <div class="tune-info">
  732.           <strong>Active Tune:</strong> <span id="activeTuneName">Performance</span><br>
  733.           <span id="tuneDescription">Balanced performance for street use</span>
  734.         </div>
  735.  
  736.         <h3>Preset Tunes</h3>
  737.         <div class="preset-grid">
  738.           <button class="btn btn-info" onclick="applyPreset('economy')">Economy</button>
  739.           <button class="btn btn-success" onclick="applyPreset('performance')">Performance</button>
  740.           <button class="btn btn-warning" onclick="applyPreset('race')">Race</button>
  741.           <button class="btn btn-primary" onclick="applyPreset('methanol')">Methanol</button>
  742.           <button class="btn btn-primary" onclick="applyPreset('gasoline')">Gasoline</button>
  743.         </div>
  744.  
  745.         <h3>Lighting Controls</h3>
  746.         <div class="lighting-grid">
  747.           <div class="light-item">
  748.             <div>Headlights</div>
  749.             <div class="light-status status-off" id="headlightStatus">OFF</div>
  750.             <button class="btn btn-primary" onclick="toggleHeadlights()">Toggle</button>
  751.           </div>
  752.           <div class="light-item">
  753.             <div>High Beams</div>
  754.             <div class="light-status status-off" id="highbeamStatus">OFF</div>
  755.             <button class="btn btn-primary" onclick="toggleHighbeams()">Toggle</button>
  756.           </div>
  757.           <div class="light-item">
  758.             <div>Left Turn</div>
  759.             <div class="light-status status-off" id="leftTurnStatus">OFF</div>
  760.             <button class="btn btn-warning" onclick="toggleLeftTurn()">Toggle</button>
  761.           </div>
  762.           <div class="light-item">
  763.             <div>Right Turn</div>
  764.             <div class="light-status status-off" id="rightTurnStatus">OFF</div>
  765.             <button class="btn btn-warning" onclick="toggleRightTurn()">Toggle</button>
  766.           </div>
  767.           <div class="light-item">
  768.             <div>Brake Lights</div>
  769.             <div class="light-status status-off" id="brakeLightStatus">OFF</div>
  770.             <button class="btn btn-danger" onclick="toggleBrakeLights()">Toggle</button>
  771.           </div>
  772.           <div class="light-item">
  773.             <div>Hazard Lights</div>
  774.             <div class="light-status status-off" id="hazardStatus">OFF</div>
  775.             <button class="btn btn-warning" onclick="toggleHazards()">Toggle</button>
  776.           </div>
  777.         </div>
  778.  
  779.         <h3>Quick Tuning</h3>
  780.         <div class="slider-container">
  781.           <label>Fuel Multiplier: <span class="slider-value" id="fuelMultiplierValue">1.00</span></label>
  782.           <input type="range" id="fuelMultiplier" min="0.5" max="1.8" step="0.01" value="1.0" oninput="updateFuelMultiplier(this.value)">
  783.         </div>
  784.  
  785.         <div class="slider-container">
  786.           <label>Ignition Advance: <span class="slider-value" id="ignitionAdvanceValue">12.0</span></label>
  787.           <input type="range" id="ignitionAdvance" min="5.0" max="35.0" step="0.5" value="12.0" oninput="updateIgnitionAdvance(this.value)">
  788.         </div>
  789.  
  790.         <div class="slider-container">
  791.           <label>Target AFR: <span class="slider-value" id="targetAFRValue">14.7</span></label>
  792.           <input type="range" id="targetAFR" min="6.0" max="18.0" step="0.1" value="14.7" oninput="updateTargetAFR(this.value)">
  793.         </div>
  794.  
  795.         <div class="slider-container">
  796.           <label>RPM Limit: <span class="slider-value" id="rpmLimitValue">8000</span></label>
  797.           <input type="range" id="rpmLimit" min="3000" max="12000" step="100" value="8000" oninput="updateRPMLimit(this.value)">
  798.         </div>
  799.  
  800.         <div class="control-group">
  801.           <button class="btn btn-primary" onclick="saveTuning()">Save Tuning</button>
  802.           <button class="btn btn-primary" onclick="loadTuning()">Load Tuning</button>
  803.           <button class="btn btn-danger" onclick="resetTuning()">Reset Defaults</button>
  804.         </div>
  805.       </div>
  806.     </div>
  807.  
  808.     <!-- Map Selection Panel -->
  809.     <div class="card">
  810.       <h2>Map Selection</h2>
  811.      
  812.       <div class="map-tabs">
  813.         <button class="map-tab fuel active" onclick="switchMapType('fuel')">Fuel Maps</button>
  814.         <button class="map-tab ignition" onclick="switchMapType('ignition')">Ignition Maps</button>
  815.       </div>
  816.      
  817.       <!-- Fuel Map Selection -->
  818.       <div id="fuelMapSelection" class="map-selection-section">
  819.         <div style="margin-bottom: 15px;">
  820.           <strong>Active Fuel Map:</strong> <span id="selectedFuelMap">Base Fuel Map</span> |
  821.           <strong>Status:</strong> <span id="fuelMapStatus">Loaded</span>
  822.         </div>
  823.        
  824.         <div class="map-selection-grid" id="fuelMapSelectionGrid">
  825.           <!-- Fuel maps will be populated by JavaScript -->
  826.         </div>
  827.       </div>
  828.      
  829.       <!-- Ignition Map Selection -->
  830.       <div id="ignitionMapSelection" class="map-selection-section" style="display: none;">
  831.         <div style="margin-bottom: 15px;">
  832.           <strong>Active Ignition Map:</strong> <span id="selectedIgnitionMap">Base Ignition Map</span> |
  833.           <strong>Status:</strong> <span id="ignitionMapStatus">Loaded</span>
  834.         </div>
  835.        
  836.         <div class="map-selection-grid" id="ignitionMapSelectionGrid">
  837.           <!-- Ignition maps will be populated by JavaScript -->
  838.         </div>
  839.       </div>
  840.      
  841.       <div class="control-group" style="margin-top: 15px;">
  842.         <button class="btn btn-primary" onclick="showUploadDialog()">Upload Custom Map</button>
  843.         <button class="btn btn-info" onclick="createNewMap()">Create New Map</button>
  844.         <button class="btn btn-warning" onclick="flashToECU()">Flash Selected to ECU</button>
  845.       </div>
  846.     </div>
  847.  
  848.     <!-- Professional Map Editor -->
  849.     <div class="card">
  850.       <h2>Professional Map Editor</h2>
  851.      
  852.       <div class="map-tabs">
  853.         <button class="map-tab fuel active" onclick="switchEditor('fuel')">Fuel Map Editor</button>
  854.         <button class="map-tab ignition" onclick="switchEditor('ignition')">Ignition Map Editor</button>
  855.       </div>
  856.      
  857.       <!-- Fuel Map Editor -->
  858.       <div id="fuelMapEditor" class="map-editor-section active">
  859.         <div class="map-controls">
  860.           <div>
  861.             <strong>Editing:</strong> <span id="editingFuelMapName">Base Fuel Map</span>
  862.           </div>
  863.           <div class="view-toggle">
  864.             <button class="active" onclick="switchFuelView('table')">Table View</button>
  865.             <button onclick="switchFuelView('3d')">3D View</button>
  866.             <button onclick="switchFuelView('graph')">Graph View</button>
  867.           </div>
  868.           <div>
  869.             <button class="btn btn-fuel" onclick="saveCurrentFuelMap()">Save Fuel Map</button>
  870.             <button class="btn btn-info" onclick="exportFuelMap()">Export Fuel Map</button>
  871.           </div>
  872.         </div>
  873.        
  874.         <!-- Table View -->
  875.         <div id="fuelTableView">
  876.           <div class="map-container">
  877.             <table class="map-table" id="fuelMapTable">
  878.               <thead id="fuelMapHeader">
  879.                 <!-- Header will be populated by JavaScript -->
  880.               </thead>
  881.               <tbody id="fuelMapBody">
  882.                 <!-- Fuel map will be populated by JavaScript -->
  883.               </tbody>
  884.             </table>
  885.           </div>
  886.         </div>
  887.        
  888.         <!-- 3D View -->
  889.         <div id="fuelView3d" style="display: none;">
  890.           <div class="map-3d" id="fuel3dContainer">
  891.             <div class="viz-controls">
  892.               <button onclick="resetFuel3DView()">Reset View</button>
  893.               <button onclick="toggleFuelWireframe()">Wireframe</button>
  894.             </div>
  895.           </div>
  896.         </div>
  897.        
  898.         <!-- Graph View -->
  899.         <div id="fuelViewGraph" style="display: none;">
  900.           <div class="map-graph">
  901.             <canvas id="fuelGraphCanvas"></canvas>
  902.           </div>
  903.         </div>
  904.        
  905.         <div class="control-group" style="margin-top: 15px;">
  906.           <button class="btn btn-fuel" onclick="updateFuelMap()">Update Fuel Map</button>
  907.           <button class="btn btn-success" onclick="autoTuneFuel()">Auto-Tune Fuel</button>
  908.           <button class="btn btn-info" onclick="smoothFuelMap()">Smooth Fuel Map</button>
  909.           <button class="btn btn-warning" onclick="resetFuelMap()">Reset Fuel Map</button>
  910.           <button class="btn btn-danger" onclick="flashFuelToECU()">Flash Fuel to ECU</button>
  911.         </div>
  912.  
  913.         <div class="control-group" style="margin-top: 15px;">
  914.           <button class="btn btn-primary" onclick="addFuelRPMRow()">Add RPM Point</button>
  915.           <button class="btn btn-primary" onclick="removeFuelRPMRow()">Remove RPM Point</button>
  916.           <button class="btn btn-info" onclick="exportFuelMap()">Export Fuel Map</button>
  917.           <button class="btn btn-info" onclick="importFuelMap()">Import Fuel Map</button>
  918.         </div>
  919.       </div>
  920.      
  921.       <!-- Ignition Map Editor -->
  922.       <div id="ignitionMapEditor" class="map-editor-section">
  923.         <div class="map-controls">
  924.           <div>
  925.             <strong>Editing:</strong> <span id="editingIgnitionMapName">Base Ignition Map</span>
  926.           </div>
  927.           <div class="view-toggle">
  928.             <button class="active" onclick="switchIgnitionView('table')">Table View</button>
  929.             <button onclick="switchIgnitionView('3d')">3D View</button>
  930.             <button onclick="switchIgnitionView('graph')">Graph View</button>
  931.           </div>
  932.           <div>
  933.             <button class="btn btn-ignition" onclick="saveCurrentIgnitionMap()">Save Ignition Map</button>
  934.             <button class="btn btn-info" onclick="exportIgnitionMap()">Export Ignition Map</button>
  935.           </div>
  936.         </div>
  937.        
  938.         <!-- Table View -->
  939.         <div id="ignitionTableView">
  940.           <div class="map-container">
  941.             <table class="map-table" id="ignitionMapTable">
  942.               <thead id="ignitionMapHeader">
  943.                 <!-- Header will be populated by JavaScript -->
  944.               </thead>
  945.               <tbody id="ignitionMapBody">
  946.                 <!-- Ignition map will be populated by JavaScript -->
  947.               </tbody>
  948.             </table>
  949.           </div>
  950.         </div>
  951.        
  952.         <!-- 3D View -->
  953.         <div id="ignitionView3d" style="display: none;">
  954.           <div class="map-3d" id="ignition3dContainer">
  955.             <div class="viz-controls">
  956.               <button onclick="resetIgnition3DView()">Reset View</button>
  957.               <button onclick="toggleIgnitionWireframe()">Wireframe</button>
  958.             </div>
  959.           </div>
  960.         </div>
  961.        
  962.         <!-- Graph View -->
  963.         <div id="ignitionViewGraph" style="display: none;">
  964.           <div class="map-graph">
  965.             <canvas id="ignitionGraphCanvas"></canvas>
  966.           </div>
  967.         </div>
  968.        
  969.         <div class="control-group" style="margin-top: 15px;">
  970.           <button class="btn btn-ignition" onclick="updateIgnitionMap()">Update Ignition Map</button>
  971.           <button class="btn btn-success" onclick="autoTuneIgnition()">Auto-Tune Ignition</button>
  972.           <button class="btn btn-info" onclick="smoothIgnitionMap()">Smooth Ignition Map</button>
  973.           <button class="btn btn-warning" onclick="resetIgnitionMap()">Reset Ignition Map</button>
  974.           <button class="btn btn-danger" onclick="flashIgnitionToECU()">Flash Ignition to ECU</button>
  975.         </div>
  976.  
  977.         <div class="control-group" style="margin-top: 15px;">
  978.           <button class="btn btn-primary" onclick="addIgnitionRPMRow()">Add RPM Point</button>
  979.           <button class="btn btn-primary" onclick="removeIgnitionRPMRow()">Remove RPM Point</button>
  980.           <button class="btn btn-info" onclick="exportIgnitionMap()">Export Ignition Map</button>
  981.           <button class="btn btn-info" onclick="importIgnitionMap()">Import Ignition Map</button>
  982.         </div>
  983.       </div>
  984.     </div>
  985.   </div>
  986.  
  987.   <!-- Upload Dialog -->
  988.   <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;">
  989.     <div style="background: white; padding: 25px; border-radius: 15px; width: 90%; max-width: 500px;">
  990.       <h3>Upload Custom Map</h3>
  991.       <div style="margin: 15px 0;">
  992.         <input type="file" id="mapFile" accept=".json,.bin,.hex" style="margin: 10px 0;">
  993.         <div style="margin: 10px 0;">
  994.           <label>Map Type:</label>
  995.           <select id="mapType" style="width: 100%; padding: 8px; margin: 5px 0; border: 1px solid #ddd; border-radius: 4px;">
  996.             <option value="fuel">Fuel Map</option>
  997.             <option value="ignition">Ignition Map</option>
  998.           </select>
  999.         </div>
  1000.         <div style="margin: 10px 0;">
  1001.           <label>Map Name:</label>
  1002.           <input type="text" id="newMapName" style="width: 100%; padding: 8px; margin: 5px 0; border: 1px solid #ddd; border-radius: 4px;">
  1003.         </div>
  1004.         <div style="margin: 10px 0;">
  1005.           <label>Description:</label>
  1006.           <textarea id="newMapDescription" style="width: 100%; padding: 8px; margin: 5px 0; border: 1px solid #ddd; border-radius: 4px; height: 80px;"></textarea>
  1007.         </div>
  1008.       </div>
  1009.       <div class="control-group">
  1010.         <button class="btn btn-primary" onclick="uploadMap()">Upload</button>
  1011.         <button class="btn btn-danger" onclick="hideUploadDialog()">Cancel</button>
  1012.       </div>
  1013.     </div>
  1014.   </div>
  1015.  
  1016.   <script>
  1017.     let fuelMapData = {};
  1018.     let ignitionMapData = {};
  1019.     let currentRpmLimit = 8000;
  1020.     let currentRpmStep = 250;
  1021.     let currentLoadStep = 10;
  1022.     let currentFuelView = 'table';
  1023.     let currentIgnitionView = 'table';
  1024.     let currentMapType = 'fuel';
  1025.     let currentEditor = 'fuel';
  1026.  
  1027.     // Three.js variables
  1028.     let fuelScene, fuelCamera, fuelRenderer, fuelMesh;
  1029.     let ignitionScene, ignitionCamera, ignitionRenderer, ignitionMesh;
  1030.     let fuelWireframe = false;
  1031.     let ignitionWireframe = false;
  1032.  
  1033.     // Chart.js variables
  1034.     let fuelChart = null;
  1035.     let ignitionChart = null;
  1036.  
  1037.     // Fuel Map Management
  1038.     let fuelMaps = {
  1039.       'base': {
  1040.         name: 'Base Fuel Map',
  1041.         description: 'Default 4-cylinder fuel map',
  1042.         type: 'base',
  1043.         editable: true,
  1044.         data: []
  1045.       },
  1046.       'economy': {
  1047.         name: 'Economy',
  1048.         description: 'Fuel efficiency focused map',
  1049.         type: 'base',
  1050.         editable: true,
  1051.         data: []
  1052.       },
  1053.       'race': {
  1054.         name: 'Race',
  1055.         description: 'Aggressive race tuning',
  1056.         type: 'base',
  1057.         editable: true,
  1058.         data: []
  1059.       }
  1060.     };
  1061.    
  1062.     // Ignition Map Management
  1063.     let ignitionMaps = {
  1064.       'base': {
  1065.         name: 'Base Ignition Map',
  1066.         description: 'Default 4-cylinder ignition map',
  1067.         type: 'base',
  1068.         editable: true,
  1069.         data: []
  1070.       },
  1071.       'economy': {
  1072.         name: 'Economy',
  1073.         description: 'Fuel efficiency focused ignition',
  1074.         type: 'base',
  1075.         editable: true,
  1076.         data: []
  1077.       },
  1078.       'race': {
  1079.         name: 'Race',
  1080.         description: 'Aggressive race ignition',
  1081.         type: 'base',
  1082.         editable: true,
  1083.         data: []
  1084.       }
  1085.     };
  1086.    
  1087.     let currentFuelMapId = 'base';
  1088.     let currentIgnitionMapId = 'base';
  1089.  
  1090.     // Initialize fuel map
  1091.     function initializeFuelMap() {
  1092.       loadFuelMapFromECU();
  1093.     }
  1094.  
  1095.     // Initialize ignition map
  1096.     function initializeIgnitionMap() {
  1097.       loadIgnitionMapFromECU();
  1098.     }
  1099.  
  1100.     function loadFuelMapFromECU() {
  1101.       fetch('/fuelMap')
  1102.         .then(response => response.json())
  1103.         .then(data => {
  1104.           fuelMapData = data;
  1105.           currentRpmLimit = data.rpmLimit || 8000;
  1106.           currentRpmStep = data.rpmStep || 250;
  1107.           currentLoadStep = data.loadStep || 10;
  1108.           renderFuelMap();
  1109.           if (currentFuelView === '3d') {
  1110.             initFuel3DView();
  1111.           } else if (currentFuelView === 'graph') {
  1112.             initFuelGraph();
  1113.           }
  1114.         })
  1115.         .catch(error => {
  1116.           console.error('Error loading fuel map:', error);
  1117.           generateDefaultFuelMap();
  1118.         });
  1119.     }
  1120.  
  1121.     function loadIgnitionMapFromECU() {
  1122.       fetch('/ignitionMap')
  1123.         .then(response => response.json())
  1124.         .then(data => {
  1125.           ignitionMapData = data;
  1126.           renderIgnitionMap();
  1127.           if (currentIgnitionView === '3d') {
  1128.             initIgnition3DView();
  1129.           } else if (currentIgnitionView === 'graph') {
  1130.             initIgnitionGraph();
  1131.           }
  1132.         })
  1133.         .catch(error => {
  1134.           console.error('Error loading ignition map:', error);
  1135.           generateDefaultIgnitionMap();
  1136.         });
  1137.     }
  1138.  
  1139.     function generateDefaultFuelMap() {
  1140.       fuelMapData = {
  1141.         rpmPoints: [],
  1142.         loadPoints: [],
  1143.         cells: {},
  1144.         rpmLimit: 8000,
  1145.         rpmStep: 250,
  1146.         loadStep: 10
  1147.       };
  1148.  
  1149.       // Generate RPM points from 0 to limit
  1150.       for (let rpm = 0; rpm <= currentRpmLimit; rpm += currentRpmStep) {
  1151.         fuelMapData.rpmPoints.push(rpm);
  1152.       }
  1153.  
  1154.       // Generate load points from 0 to 100%
  1155.       for (let load = 0; load <= 100; load += currentLoadStep) {
  1156.         fuelMapData.loadPoints.push(load);
  1157.       }
  1158.  
  1159.       // Initialize cells with default values
  1160.       fuelMapData.rpmPoints.forEach(rpm => {
  1161.         fuelMapData.cells[rpm] = {};
  1162.         fuelMapData.loadPoints.forEach(load => {
  1163.           // Base calculation for pulse width
  1164.           let baseValue = 800 + (rpm * 0.1) + (load * 8);
  1165.           fuelMapData.cells[rpm][load] = Math.round(baseValue);
  1166.         });
  1167.       });
  1168.  
  1169.       renderFuelMap();
  1170.     }
  1171.  
  1172.     function generateDefaultIgnitionMap() {
  1173.       ignitionMapData = {
  1174.         rpmPoints: [],
  1175.         loadPoints: [],
  1176.         cells: {},
  1177.         rpmLimit: 8000,
  1178.         rpmStep: 250,
  1179.         loadStep: 10
  1180.       };
  1181.  
  1182.       // Generate RPM points from 0 to limit
  1183.       for (let rpm = 0; rpm <= currentRpmLimit; rpm += currentRpmStep) {
  1184.         ignitionMapData.rpmPoints.push(rpm);
  1185.       }
  1186.  
  1187.       // Generate load points from 0 to 100%
  1188.       for (let load = 0; load <= 100; load += currentLoadStep) {
  1189.         ignitionMapData.loadPoints.push(load);
  1190.       }
  1191.  
  1192.       // Initialize cells with default values
  1193.       ignitionMapData.rpmPoints.forEach(rpm => {
  1194.         ignitionMapData.cells[rpm] = {};
  1195.         ignitionMapData.loadPoints.forEach(load => {
  1196.           // Base calculation for ignition advance
  1197.           let baseValue = 10 + (rpm * 0.001) + (load * 0.1);
  1198.           ignitionMapData.cells[rpm][load] = parseFloat(baseValue.toFixed(1));
  1199.         });
  1200.       });
  1201.  
  1202.       renderIgnitionMap();
  1203.     }
  1204.  
  1205.     function renderFuelMap() {
  1206.       const header = document.getElementById('fuelMapHeader');
  1207.       const body = document.getElementById('fuelMapBody');
  1208.      
  1209.       // Clear existing content
  1210.       header.innerHTML = '';
  1211.       body.innerHTML = '';
  1212.  
  1213.       // Create header row
  1214.       let headerRow = '<tr><th>RPM/Load</th>';
  1215.       fuelMapData.loadPoints.forEach(load => {
  1216.         headerRow += `<th>${load}%</th>`;
  1217.       });
  1218.       headerRow += '</tr>';
  1219.       header.innerHTML = headerRow;
  1220.  
  1221.       // Create data rows
  1222.       fuelMapData.rpmPoints.forEach(rpm => {
  1223.         let row = `<tr><td><strong>${rpm}</strong></td>`;
  1224.         fuelMapData.loadPoints.forEach(load => {
  1225.           const value = fuelMapData.cells[rpm] && fuelMapData.cells[rpm][load] ? fuelMapData.cells[rpm][load] : 0;
  1226.           row += `<td><input type="number" value="${value}" data-rpm="${rpm}" data-load="${load}"></td>`;
  1227.         });
  1228.         row += '</tr>';
  1229.         body.innerHTML += row;
  1230.       });
  1231.  
  1232.       // Update UI info
  1233.       document.getElementById('selectedFuelMap').textContent = 'Base Fuel Map';
  1234.     }
  1235.  
  1236.     function renderIgnitionMap() {
  1237.       const header = document.getElementById('ignitionMapHeader');
  1238.       const body = document.getElementById('ignitionMapBody');
  1239.      
  1240.       // Clear existing content
  1241.       header.innerHTML = '';
  1242.       body.innerHTML = '';
  1243.  
  1244.       // Create header row
  1245.       let headerRow = '<tr><th>RPM/Load</th>';
  1246.       ignitionMapData.loadPoints.forEach(load => {
  1247.         headerRow += `<th>${load}%</th>`;
  1248.       });
  1249.       headerRow += '</tr>';
  1250.       header.innerHTML = headerRow;
  1251.  
  1252.       // Create data rows
  1253.       ignitionMapData.rpmPoints.forEach(rpm => {
  1254.         let row = `<tr><td><strong>${rpm}</strong></td>`;
  1255.         ignitionMapData.loadPoints.forEach(load => {
  1256.           const value = ignitionMapData.cells[rpm] && ignitionMapData.cells[rpm][load] ? ignitionMapData.cells[rpm][load] : 0;
  1257.           row += `<td><input type="number" step="0.1" value="${value}" data-rpm="${rpm}" data-load="${load}"></td>`;
  1258.         });
  1259.         row += '</tr>';
  1260.         body.innerHTML += row;
  1261.       });
  1262.  
  1263.       // Update UI info
  1264.       document.getElementById('selectedIgnitionMap').textContent = 'Base Ignition Map';
  1265.     }
  1266.  
  1267.     // 3D Visualization Functions
  1268.     function initFuel3DView() {
  1269.       const container = document.getElementById('fuel3dContainer');
  1270.       container.innerHTML = '<canvas id="fuel3dCanvas"></canvas>';
  1271.      
  1272.       const canvas = document.getElementById('fuel3dCanvas');
  1273.       canvas.width = container.clientWidth;
  1274.       canvas.height = container.clientHeight;
  1275.  
  1276.       // Create scene
  1277.       fuelScene = new THREE.Scene();
  1278.       fuelScene.background = new THREE.Color(0x1a1a1a);
  1279.  
  1280.       // Create camera
  1281.       fuelCamera = new THREE.PerspectiveCamera(75, canvas.width / canvas.height, 0.1, 1000);
  1282.       fuelCamera.position.set(50, 50, 80);
  1283.       fuelCamera.lookAt(0, 0, 0);
  1284.  
  1285.       // Create renderer
  1286.       fuelRenderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true });
  1287.       fuelRenderer.setSize(canvas.width, canvas.height);
  1288.  
  1289.       // Add lights
  1290.       const ambientLight = new THREE.AmbientLight(0x404040);
  1291.       fuelScene.add(ambientLight);
  1292.  
  1293.       const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
  1294.       directionalLight.position.set(50, 50, 50);
  1295.       fuelScene.add(directionalLight);
  1296.  
  1297.       // Create grid helper
  1298.       const gridHelper = new THREE.GridHelper(100, 20, 0x444444, 0x222222);
  1299.       fuelScene.add(gridHelper);
  1300.  
  1301.       // Create axes helper
  1302.       const axesHelper = new THREE.AxesHelper(30);
  1303.       fuelScene.add(axesHelper);
  1304.  
  1305.       // Create the surface
  1306.       createFuelSurface();
  1307.  
  1308.       // Add orbit controls
  1309.       const controls = new THREE.OrbitControls(fuelCamera, fuelRenderer.domElement);
  1310.       controls.enableDamping = true;
  1311.       controls.dampingFactor = 0.05;
  1312.  
  1313.       // Animation loop
  1314.       function animate() {
  1315.         requestAnimationFrame(animate);
  1316.         controls.update();
  1317.         fuelRenderer.render(fuelScene, fuelCamera);
  1318.       }
  1319.       animate();
  1320.  
  1321.       // Handle window resize
  1322.       window.addEventListener('resize', function() {
  1323.         canvas.width = container.clientWidth;
  1324.         canvas.height = container.clientHeight;
  1325.         fuelCamera.aspect = canvas.width / canvas.height;
  1326.         fuelCamera.updateProjectionMatrix();
  1327.         fuelRenderer.setSize(canvas.width, canvas.height);
  1328.       });
  1329.     }
  1330.  
  1331.     function createFuelSurface() {
  1332.       // Remove existing mesh
  1333.       if (fuelMesh) {
  1334.         fuelScene.remove(fuelMesh);
  1335.       }
  1336.  
  1337.       const rpmPoints = fuelMapData.rpmPoints;
  1338.       const loadPoints = fuelMapData.loadPoints;
  1339.      
  1340.       if (rpmPoints.length === 0 || loadPoints.length === 0) return;
  1341.  
  1342.       // Create geometry
  1343.       const geometry = new THREE.PlaneGeometry(80, 80, rpmPoints.length - 1, loadPoints.length - 1);
  1344.       const positions = geometry.attributes.position.array;
  1345.  
  1346.       // Scale and position the surface
  1347.       const rpmScale = 80 / Math.max(...rpmPoints);
  1348.       const loadScale = 80 / 100;
  1349.       const valueScale = 0.1; // Scale for pulse width values
  1350.  
  1351.       for (let i = 0; i < positions.length; i += 3) {
  1352.         const vertexIndex = i / 3;
  1353.         const x = positions[i];
  1354.         const y = positions[i + 1];
  1355.        
  1356.         // Convert geometry coordinates to map indices
  1357.         const rpmIndex = Math.round((x + 40) / 80 * (rpmPoints.length - 1));
  1358.         const loadIndex = Math.round((y + 40) / 80 * (loadPoints.length - 1));
  1359.        
  1360.         if (rpmIndex >= 0 && rpmIndex < rpmPoints.length && loadIndex >= 0 && loadIndex < loadPoints.length) {
  1361.           const rpm = rpmPoints[rpmIndex];
  1362.           const load = loadPoints[loadIndex];
  1363.           const value = fuelMapData.cells[rpm] && fuelMapData.cells[rpm][load] ? fuelMapData.cells[rpm][load] : 0;
  1364.          
  1365.           positions[i + 2] = value * valueScale; // Set Z position based on pulse width
  1366.         }
  1367.       }
  1368.  
  1369.       geometry.computeVertexNormals();
  1370.  
  1371.       // Create material
  1372.       const material = new THREE.MeshPhongMaterial({
  1373.         color: 0xe74c3c,
  1374.         wireframe: fuelWireframe,
  1375.         side: THREE.DoubleSide,
  1376.         transparent: true,
  1377.         opacity: 0.8
  1378.       });
  1379.  
  1380.       // Create mesh
  1381.       fuelMesh = new THREE.Mesh(geometry, material);
  1382.       fuelMesh.rotation.x = -Math.PI / 2;
  1383.       fuelScene.add(fuelMesh);
  1384.  
  1385.       // Add labels
  1386.       add3DLabels(fuelScene, 'Fuel Map', 'RPM', 'Load', 'Pulse Width (µs)');
  1387.     }
  1388.  
  1389.     function initIgnition3DView() {
  1390.       const container = document.getElementById('ignition3dContainer');
  1391.       container.innerHTML = '<canvas id="ignition3dCanvas"></canvas>';
  1392.      
  1393.       const canvas = document.getElementById('ignition3dCanvas');
  1394.       canvas.width = container.clientWidth;
  1395.       canvas.height = container.clientHeight;
  1396.  
  1397.       // Create scene
  1398.       ignitionScene = new THREE.Scene();
  1399.       ignitionScene.background = new THREE.Color(0x1a1a1a);
  1400.  
  1401.       // Create camera
  1402.       ignitionCamera = new THREE.PerspectiveCamera(75, canvas.width / canvas.height, 0.1, 1000);
  1403.       ignitionCamera.position.set(50, 50, 80);
  1404.       ignitionCamera.lookAt(0, 0, 0);
  1405.  
  1406.       // Create renderer
  1407.       ignitionRenderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true });
  1408.       ignitionRenderer.setSize(canvas.width, canvas.height);
  1409.  
  1410.       // Add lights
  1411.       const ambientLight = new THREE.AmbientLight(0x404040);
  1412.       ignitionScene.add(ambientLight);
  1413.  
  1414.       const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
  1415.       directionalLight.position.set(50, 50, 50);
  1416.       ignitionScene.add(directionalLight);
  1417.  
  1418.       // Create grid helper
  1419.       const gridHelper = new THREE.GridHelper(100, 20, 0x444444, 0x222222);
  1420.       ignitionScene.add(gridHelper);
  1421.  
  1422.       // Create axes helper
  1423.       const axesHelper = new THREE.AxesHelper(30);
  1424.       ignitionScene.add(axesHelper);
  1425.  
  1426.       // Create the surface
  1427.       createIgnitionSurface();
  1428.  
  1429.       // Add orbit controls
  1430.       const controls = new THREE.OrbitControls(ignitionCamera, ignitionRenderer.domElement);
  1431.       controls.enableDamping = true;
  1432.       controls.dampingFactor = 0.05;
  1433.  
  1434.       // Animation loop
  1435.       function animate() {
  1436.         requestAnimationFrame(animate);
  1437.         controls.update();
  1438.         ignitionRenderer.render(ignitionScene, ignitionCamera);
  1439.       }
  1440.       animate();
  1441.  
  1442.       // Handle window resize
  1443.       window.addEventListener('resize', function() {
  1444.         canvas.width = container.clientWidth;
  1445.         canvas.height = container.clientHeight;
  1446.         ignitionCamera.aspect = canvas.width / canvas.height;
  1447.         ignitionCamera.updateProjectionMatrix();
  1448.         ignitionRenderer.setSize(canvas.width, canvas.height);
  1449.       });
  1450.     }
  1451.  
  1452.     function createIgnitionSurface() {
  1453.       // Remove existing mesh
  1454.       if (ignitionMesh) {
  1455.         ignitionScene.remove(ignitionMesh);
  1456.       }
  1457.  
  1458.       const rpmPoints = ignitionMapData.rpmPoints;
  1459.       const loadPoints = ignitionMapData.loadPoints;
  1460.      
  1461.       if (rpmPoints.length === 0 || loadPoints.length === 0) return;
  1462.  
  1463.       // Create geometry
  1464.       const geometry = new THREE.PlaneGeometry(80, 80, rpmPoints.length - 1, loadPoints.length - 1);
  1465.       const positions = geometry.attributes.position.array;
  1466.  
  1467.       // Scale and position the surface
  1468.       const rpmScale = 80 / Math.max(...rpmPoints);
  1469.       const loadScale = 80 / 100;
  1470.       const valueScale = 2; // Scale for ignition advance values
  1471.  
  1472.       for (let i = 0; i < positions.length; i += 3) {
  1473.         const vertexIndex = i / 3;
  1474.         const x = positions[i];
  1475.         const y = positions[i + 1];
  1476.        
  1477.         // Convert geometry coordinates to map indices
  1478.         const rpmIndex = Math.round((x + 40) / 80 * (rpmPoints.length - 1));
  1479.         const loadIndex = Math.round((y + 40) / 80 * (loadPoints.length - 1));
  1480.        
  1481.         if (rpmIndex >= 0 && rpmIndex < rpmPoints.length && loadIndex >= 0 && loadIndex < loadPoints.length) {
  1482.           const rpm = rpmPoints[rpmIndex];
  1483.           const load = loadPoints[loadIndex];
  1484.           const value = ignitionMapData.cells[rpm] && ignitionMapData.cells[rpm][load] ? ignitionMapData.cells[rpm][load] : 0;
  1485.          
  1486.           positions[i + 2] = value * valueScale; // Set Z position based on ignition advance
  1487.         }
  1488.       }
  1489.  
  1490.       geometry.computeVertexNormals();
  1491.  
  1492.       // Create material
  1493.       const material = new THREE.MeshPhongMaterial({
  1494.         color: 0x3498db,
  1495.         wireframe: ignitionWireframe,
  1496.         side: THREE.DoubleSide,
  1497.         transparent: true,
  1498.         opacity: 0.8
  1499.       });
  1500.  
  1501.       // Create mesh
  1502.       ignitionMesh = new THREE.Mesh(geometry, material);
  1503.       ignitionMesh.rotation.x = -Math.PI / 2;
  1504.       ignitionScene.add(ignitionMesh);
  1505.  
  1506.       // Add labels
  1507.       add3DLabels(ignitionScene, 'Ignition Map', 'RPM', 'Load', 'Advance (°)');
  1508.     }
  1509.  
  1510.     function add3DLabels(scene, title, xLabel, yLabel, zLabel) {
  1511.       // Simple text labels using HTML would be better, but for simplicity we'll skip detailed labels
  1512.     }
  1513.  
  1514.     function resetFuel3DView() {
  1515.       if (fuelCamera) {
  1516.         fuelCamera.position.set(50, 50, 80);
  1517.         fuelCamera.lookAt(0, 0, 0);
  1518.       }
  1519.     }
  1520.  
  1521.     function resetIgnition3DView() {
  1522.       if (ignitionCamera) {
  1523.         ignitionCamera.position.set(50, 50, 80);
  1524.         ignitionCamera.lookAt(0, 0, 0);
  1525.       }
  1526.     }
  1527.  
  1528.     function toggleFuelWireframe() {
  1529.       fuelWireframe = !fuelWireframe;
  1530.       if (fuelMesh) {
  1531.         fuelMesh.material.wireframe = fuelWireframe;
  1532.       }
  1533.     }
  1534.  
  1535.     function toggleIgnitionWireframe() {
  1536.       ignitionWireframe = !ignitionWireframe;
  1537.       if (ignitionMesh) {
  1538.         ignitionMesh.material.wireframe = ignitionWireframe;
  1539.       }
  1540.     }
  1541.  
  1542.     // Graph Visualization Functions
  1543.     function initFuelGraph() {
  1544.       const ctx = document.getElementById('fuelGraphCanvas').getContext('2d');
  1545.      
  1546.       if (fuelChart) {
  1547.         fuelChart.destroy();
  1548.       }
  1549.  
  1550.       const rpmPoints = fuelMapData.rpmPoints;
  1551.       const loadPoints = fuelMapData.loadPoints;
  1552.      
  1553.       if (rpmPoints.length === 0 || loadPoints.length === 0) return;
  1554.  
  1555.       const datasets = [];
  1556.       const loadColors = [
  1557.         '#ff4444', '#ff8844', '#ffcc44', '#ffff44',
  1558.         '#ccff44', '#88ff44', '#44ff44', '#44ff88',
  1559.         '#44ffcc', '#44ffff', '#4488ff'
  1560.       ];
  1561.  
  1562.       loadPoints.forEach((load, index) => {
  1563.         const data = rpmPoints.map(rpm => {
  1564.           return fuelMapData.cells[rpm] && fuelMapData.cells[rpm][load] ? fuelMapData.cells[rpm][load] : 0;
  1565.         });
  1566.  
  1567.         datasets.push({
  1568.           label: `${load}% Load`,
  1569.           data: data,
  1570.           borderColor: loadColors[index % loadColors.length],
  1571.           backgroundColor: loadColors[index % loadColors.length] + '20',
  1572.           borderWidth: 2,
  1573.           tension: 0.4,
  1574.           fill: false
  1575.         });
  1576.       });
  1577.  
  1578.       fuelChart = new Chart(ctx, {
  1579.         type: 'line',
  1580.         data: {
  1581.           labels: rpmPoints,
  1582.           datasets: datasets
  1583.         },
  1584.         options: {
  1585.           responsive: true,
  1586.           maintainAspectRatio: false,
  1587.           plugins: {
  1588.             title: {
  1589.               display: true,
  1590.               text: 'Fuel Map - Pulse Width vs RPM',
  1591.               color: '#333',
  1592.               font: {
  1593.                 size: 16
  1594.               }
  1595.             },
  1596.             legend: {
  1597.               position: 'right',
  1598.               labels: {
  1599.                 color: '#333',
  1600.                 usePointStyle: true
  1601.               }
  1602.             }
  1603.           },
  1604.           scales: {
  1605.             x: {
  1606.               title: {
  1607.                 display: true,
  1608.                 text: 'RPM',
  1609.                 color: '#333'
  1610.               },
  1611.               grid: {
  1612.                 color: '#e0e0e0'
  1613.               },
  1614.               ticks: {
  1615.                 color: '#666'
  1616.               }
  1617.             },
  1618.             y: {
  1619.               title: {
  1620.                 display: true,
  1621.                 text: 'Pulse Width (µs)',
  1622.                 color: '#333'
  1623.               },
  1624.               grid: {
  1625.                 color: '#e0e0e0'
  1626.               },
  1627.               ticks: {
  1628.                 color: '#666'
  1629.               }
  1630.             }
  1631.           }
  1632.         }
  1633.       });
  1634.     }
  1635.  
  1636.     function initIgnitionGraph() {
  1637.       const ctx = document.getElementById('ignitionGraphCanvas').getContext('2d');
  1638.      
  1639.       if (ignitionChart) {
  1640.         ignitionChart.destroy();
  1641.       }
  1642.  
  1643.       const rpmPoints = ignitionMapData.rpmPoints;
  1644.       const loadPoints = ignitionMapData.loadPoints;
  1645.      
  1646.       if (rpmPoints.length === 0 || loadPoints.length === 0) return;
  1647.  
  1648.       const datasets = [];
  1649.       const loadColors = [
  1650.         '#4444ff', '#4488ff', '#44ccff', '#44ffff',
  1651.         '#44ffcc', '#44ff88', '#44ff44', '#88ff44',
  1652.         '#ccff44', '#ffff44', '#ffcc44'
  1653.       ];
  1654.  
  1655.       loadPoints.forEach((load, index) => {
  1656.         const data = rpmPoints.map(rpm => {
  1657.           return ignitionMapData.cells[rpm] && ignitionMapData.cells[rpm][load] ? ignitionMapData.cells[rpm][load] : 0;
  1658.         });
  1659.  
  1660.         datasets.push({
  1661.           label: `${load}% Load`,
  1662.           data: data,
  1663.           borderColor: loadColors[index % loadColors.length],
  1664.           backgroundColor: loadColors[index % loadColors.length] + '20',
  1665.           borderWidth: 2,
  1666.           tension: 0.4,
  1667.           fill: false
  1668.         });
  1669.       });
  1670.  
  1671.       ignitionChart = new Chart(ctx, {
  1672.         type: 'line',
  1673.         data: {
  1674.           labels: rpmPoints,
  1675.           datasets: datasets
  1676.         },
  1677.         options: {
  1678.           responsive: true,
  1679.           maintainAspectRatio: false,
  1680.           plugins: {
  1681.             title: {
  1682.               display: true,
  1683.               text: 'Ignition Map - Advance vs RPM',
  1684.               color: '#333',
  1685.               font: {
  1686.                 size: 16
  1687.               }
  1688.             },
  1689.             legend: {
  1690.               position: 'right',
  1691.               labels: {
  1692.                 color: '#333',
  1693.                 usePointStyle: true
  1694.               }
  1695.             }
  1696.           },
  1697.           scales: {
  1698.             x: {
  1699.               title: {
  1700.                 display: true,
  1701.                 text: 'RPM',
  1702.                 color: '#333'
  1703.               },
  1704.               grid: {
  1705.                 color: '#e0e0e0'
  1706.               },
  1707.               ticks: {
  1708.                 color: '#666'
  1709.               }
  1710.             },
  1711.             y: {
  1712.               title: {
  1713.                 display: true,
  1714.                 text: 'Ignition Advance (°)',
  1715.                 color: '#333'
  1716.               },
  1717.               grid: {
  1718.                 color: '#e0e0e0'
  1719.               },
  1720.               ticks: {
  1721.                 color: '#666'
  1722.               }
  1723.             }
  1724.           }
  1725.         }
  1726.       });
  1727.     }
  1728.  
  1729.     // View switching functions
  1730.     function switchFuelView(view) {
  1731.       currentFuelView = view;
  1732.      
  1733.       // Update button states
  1734.       document.querySelectorAll('#fuelMapEditor .view-toggle button').forEach(btn => {
  1735.         btn.classList.remove('active');
  1736.       });
  1737.       event.target.classList.add('active');
  1738.      
  1739.       // Show/hide views
  1740.       document.getElementById('fuelTableView').style.display = view === 'table' ? 'block' : 'none';
  1741.       document.getElementById('fuelView3d').style.display = view === '3d' ? 'block' : 'none';
  1742.       document.getElementById('fuelViewGraph').style.display = view === 'graph' ? 'block' : 'none';
  1743.      
  1744.       // Initialize visualization if needed
  1745.       if (view === '3d' && !fuelRenderer) {
  1746.         setTimeout(() => initFuel3DView(), 100);
  1747.       } else if (view === 'graph' && !fuelChart) {
  1748.         setTimeout(() => initFuelGraph(), 100);
  1749.       } else if (view === '3d' && fuelRenderer) {
  1750.         createFuelSurface();
  1751.       } else if (view === 'graph' && fuelChart) {
  1752.         fuelChart.update();
  1753.       }
  1754.     }
  1755.    
  1756.     function switchIgnitionView(view) {
  1757.       currentIgnitionView = view;
  1758.      
  1759.       // Update button states
  1760.       document.querySelectorAll('#ignitionMapEditor .view-toggle button').forEach(btn => {
  1761.         btn.classList.remove('active');
  1762.       });
  1763.       event.target.classList.add('active');
  1764.      
  1765.       // Show/hide views
  1766.       document.getElementById('ignitionTableView').style.display = view === 'table' ? 'block' : 'none';
  1767.       document.getElementById('ignitionView3d').style.display = view === '3d' ? 'block' : 'none';
  1768.       document.getElementById('ignitionViewGraph').style.display = view === 'graph' ? 'block' : 'none';
  1769.      
  1770.       // Initialize visualization if needed
  1771.       if (view === '3d' && !ignitionRenderer) {
  1772.         setTimeout(() => initIgnition3DView(), 100);
  1773.       } else if (view === 'graph' && !ignitionChart) {
  1774.         setTimeout(() => initIgnitionGraph(), 100);
  1775.       } else if (view === '3d' && ignitionRenderer) {
  1776.         createIgnitionSurface();
  1777.       } else if (view === 'graph' && ignitionChart) {
  1778.         ignitionChart.update();
  1779.       }
  1780.     }
  1781.  
  1782.     // Map Selection Functions
  1783.     function loadFuelMapSelection() {
  1784.       const grid = document.getElementById('fuelMapSelectionGrid');
  1785.       grid.innerHTML = '';
  1786.      
  1787.       for (let mapId in fuelMaps) {
  1788.         const map = fuelMaps[mapId];
  1789.         const isActive = mapId === currentFuelMapId;
  1790.        
  1791.         const mapItem = document.createElement('div');
  1792.         mapItem.className = `map-item ${isActive ? 'active' : ''}`;
  1793.         mapItem.onclick = () => selectFuelMap(mapId);
  1794.        
  1795.         mapItem.innerHTML = `
  1796.           <h3>${map.name}</h3>
  1797.           <div class="map-meta">${map.description}</div>
  1798.           <div class="map-meta">Type: ${map.type} | ${map.editable ? 'Editable' : 'Read-only'}</div>
  1799.           <div class="map-actions">
  1800.             <button class="btn btn-primary" onclick="event.stopPropagation(); loadFuelMapForEditing('${mapId}')">Edit</button>
  1801.             <button class="btn btn-info" onclick="event.stopPropagation(); exportSingleFuelMap('${mapId}')">Export</button>
  1802.             ${map.type === 'custom' ?
  1803.               `<button class="btn btn-danger" onclick="event.stopPropagation(); deleteFuelMap('${mapId}')">Delete</button>` :
  1804.               ''
  1805.             }
  1806.           </div>
  1807.         `;
  1808.        
  1809.         grid.appendChild(mapItem);
  1810.       }
  1811.     }
  1812.    
  1813.     function loadIgnitionMapSelection() {
  1814.       const grid = document.getElementById('ignitionMapSelectionGrid');
  1815.       grid.innerHTML = '';
  1816.      
  1817.       for (let mapId in ignitionMaps) {
  1818.         const map = ignitionMaps[mapId];
  1819.         const isActive = mapId === currentIgnitionMapId;
  1820.        
  1821.         const mapItem = document.createElement('div');
  1822.         mapItem.className = `map-item ${isActive ? 'active' : ''}`;
  1823.         mapItem.onclick = () => selectIgnitionMap(mapId);
  1824.        
  1825.         mapItem.innerHTML = `
  1826.           <h3>${map.name}</h3>
  1827.           <div class="map-meta">${map.description}</div>
  1828.           <div class="map-meta">Type: ${map.type} | ${map.editable ? 'Editable' : 'Read-only'}</div>
  1829.           <div class="map-actions">
  1830.             <button class="btn btn-primary" onclick="event.stopPropagation(); loadIgnitionMapForEditing('${mapId}')">Edit</button>
  1831.             <button class="btn btn-info" onclick="event.stopPropagation(); exportSingleIgnitionMap('${mapId}')">Export</button>
  1832.             ${map.type === 'custom' ?
  1833.               `<button class="btn btn-danger" onclick="event.stopPropagation(); deleteIgnitionMap('${mapId}')">Delete</button>` :
  1834.               ''
  1835.             }
  1836.           </div>
  1837.         `;
  1838.        
  1839.         grid.appendChild(mapItem);
  1840.       }
  1841.     }
  1842.    
  1843.     function selectFuelMap(mapId) {
  1844.       currentFuelMapId = mapId;
  1845.       document.getElementById('selectedFuelMap').textContent = fuelMaps[mapId].name;
  1846.       document.getElementById('fuelMapStatus').textContent = 'Loaded';
  1847.       loadFuelMapSelection();
  1848.     }
  1849.    
  1850.     function selectIgnitionMap(mapId) {
  1851.       currentIgnitionMapId = mapId;
  1852.       document.getElementById('selectedIgnitionMap').textContent = ignitionMaps[mapId].name;
  1853.       document.getElementById('ignitionMapStatus').textContent = 'Loaded';
  1854.       loadIgnitionMapSelection();
  1855.     }
  1856.    
  1857.     function loadFuelMapForEditing(mapId) {
  1858.       currentFuelMapId = mapId;
  1859.       document.getElementById('editingFuelMapName').textContent = fuelMaps[mapId].name;
  1860.       loadFuelMapFromECU();
  1861.     }
  1862.    
  1863.     function loadIgnitionMapForEditing(mapId) {
  1864.       currentIgnitionMapId = mapId;
  1865.       document.getElementById('editingIgnitionMapName').textContent = ignitionMaps[mapId].name;
  1866.       loadIgnitionMapFromECU();
  1867.     }
  1868.    
  1869.     function switchMapType(type) {
  1870.       currentMapType = type;
  1871.      
  1872.       // Update tab states
  1873.       document.querySelectorAll('.map-tab').forEach(tab => {
  1874.         tab.classList.remove('active');
  1875.       });
  1876.       event.target.classList.add('active');
  1877.      
  1878.       // Show/hide sections
  1879.       document.getElementById('fuelMapSelection').style.display = type === 'fuel' ? 'block' : 'none';
  1880.       document.getElementById('ignitionMapSelection').style.display = type === 'ignition' ? 'block' : 'none';
  1881.     }
  1882.    
  1883.     function switchEditor(type) {
  1884.       currentEditor = type;
  1885.      
  1886.       // Update tab states
  1887.       document.querySelectorAll('.map-tab').forEach(tab => {
  1888.         tab.classList.remove('active');
  1889.       });
  1890.       event.target.classList.add('active');
  1891.      
  1892.       // Show/hide sections
  1893.       document.getElementById('fuelMapEditor').style.display = type === 'fuel' ? 'block' : 'none';
  1894.       document.getElementById('ignitionMapEditor').style.display = type === 'ignition' ? 'block' : 'none';
  1895.     }
  1896.    
  1897.     function showUploadDialog() {
  1898.       document.getElementById('uploadDialog').style.display = 'flex';
  1899.     }
  1900.    
  1901.     function hideUploadDialog() {
  1902.       document.getElementById('uploadDialog').style.display = 'none';
  1903.     }
  1904.    
  1905.     function uploadMap() {
  1906.       const fileInput = document.getElementById('mapFile');
  1907.       const typeInput = document.getElementById('mapType');
  1908.       const nameInput = document.getElementById('newMapName');
  1909.       const descInput = document.getElementById('newMapDescription');
  1910.      
  1911.       if (!nameInput.value) {
  1912.         alert('Please enter a map name');
  1913.         return;
  1914.       }
  1915.      
  1916.       const mapType = typeInput.value;
  1917.       const newMapId = nameInput.value.toLowerCase().replace(/\s+/g, '_');
  1918.      
  1919.       if (mapType === 'fuel') {
  1920.         fuelMaps[newMapId] = {
  1921.           name: nameInput.value,
  1922.           description: descInput.value,
  1923.           type: 'custom',
  1924.           editable: true,
  1925.           data: fuelMapData
  1926.         };
  1927.        
  1928.         loadFuelMapSelection();
  1929.       } else {
  1930.         ignitionMaps[newMapId] = {
  1931.           name: nameInput.value,
  1932.           description: descInput.value,
  1933.           type: 'custom',
  1934.           editable: true,
  1935.           data: ignitionMapData
  1936.         };
  1937.        
  1938.         loadIgnitionMapSelection();
  1939.       }
  1940.      
  1941.       // Reset form
  1942.       fileInput.value = '';
  1943.       nameInput.value = '';
  1944.       descInput.value = '';
  1945.      
  1946.       hideUploadDialog();
  1947.       alert('Map uploaded successfully!');
  1948.     }
  1949.    
  1950.     function createNewMap() {
  1951.       const type = currentMapType;
  1952.       const name = prompt('Enter a name for the new ' + type + ' map:');
  1953.       if (!name) return;
  1954.      
  1955.       const newMapId = name.toLowerCase().replace(/\s+/g, '_');
  1956.      
  1957.       if (type === 'fuel') {
  1958.         fuelMaps[newMapId] = {
  1959.           name: name,
  1960.           description: 'Custom fuel map',
  1961.           type: 'custom',
  1962.           editable: true,
  1963.           data: fuelMapData
  1964.         };
  1965.        
  1966.         loadFuelMapSelection();
  1967.         selectFuelMap(newMapId);
  1968.       } else {
  1969.         ignitionMaps[newMapId] = {
  1970.           name: name,
  1971.           description: 'Custom ignition map',
  1972.           type: 'custom',
  1973.           editable: true,
  1974.           data: ignitionMapData
  1975.         };
  1976.        
  1977.         loadIgnitionMapSelection();
  1978.         selectIgnitionMap(newMapId);
  1979.       }
  1980.     }
  1981.    
  1982.     function deleteFuelMap(mapId) {
  1983.       if (confirm(`Are you sure you want to delete "${fuelMaps[mapId].name}"?`)) {
  1984.         delete fuelMaps[mapId];
  1985.         if (currentFuelMapId === mapId) {
  1986.           currentFuelMapId = 'base';
  1987.           selectFuelMap(currentFuelMapId);
  1988.         }
  1989.         loadFuelMapSelection();
  1990.       }
  1991.     }
  1992.    
  1993.     function deleteIgnitionMap(mapId) {
  1994.       if (confirm(`Are you sure you want to delete "${ignitionMaps[mapId].name}"?`)) {
  1995.         delete ignitionMaps[mapId];
  1996.         if (currentIgnitionMapId === mapId) {
  1997.           currentIgnitionMapId = 'base';
  1998.           selectIgnitionMap(currentIgnitionMapId);
  1999.         }
  2000.         loadIgnitionMapSelection();
  2001.       }
  2002.     }
  2003.  
  2004.     function updateFuelMap() {
  2005.       const inputs = document.querySelectorAll('#fuelMapBody input');
  2006.       const updates = [];
  2007.  
  2008.       inputs.forEach(input => {
  2009.         const rpm = parseInt(input.dataset.rpm);
  2010.         const load = parseInt(input.dataset.load);
  2011.         const value = parseInt(input.value) || 0;
  2012.        
  2013.         if (!fuelMapData.cells[rpm]) fuelMapData.cells[rpm] = {};
  2014.         fuelMapData.cells[rpm][load] = value;
  2015.        
  2016.         updates.push({ rpm, load, pulseWidth: value });
  2017.       });
  2018.  
  2019.       // Send updates to ECU
  2020.       fetch('/fuelMap/update', {
  2021.         method: 'POST',
  2022.         headers: { 'Content-Type': 'application/json' },
  2023.         body: JSON.stringify({ updates: updates })
  2024.       })
  2025.       .then(() => {
  2026.         alert('Fuel map updated successfully!');
  2027.         // Update visualizations
  2028.         if (currentFuelView === '3d') {
  2029.           createFuelSurface();
  2030.         } else if (currentFuelView === 'graph') {
  2031.           initFuelGraph();
  2032.         }
  2033.       })
  2034.       .catch(error => console.error('Error updating fuel map:', error));
  2035.     }
  2036.    
  2037.     function updateIgnitionMap() {
  2038.       const inputs = document.querySelectorAll('#ignitionMapBody input');
  2039.       const updates = [];
  2040.  
  2041.       inputs.forEach(input => {
  2042.         const rpm = parseInt(input.dataset.rpm);
  2043.         const load = parseInt(input.dataset.load);
  2044.         const value = parseFloat(input.value) || 0;
  2045.        
  2046.         if (!ignitionMapData.cells[rpm]) ignitionMapData.cells[rpm] = {};
  2047.         ignitionMapData.cells[rpm][load] = value;
  2048.        
  2049.         updates.push({ rpm, load, advance: value });
  2050.       });
  2051.  
  2052.       // Send updates to ECU
  2053.       fetch('/ignitionMap/update', {
  2054.         method: 'POST',
  2055.         headers: { 'Content-Type': 'application/json' },
  2056.         body: JSON.stringify({ updates: updates })
  2057.       })
  2058.       .then(() => {
  2059.         alert('Ignition map updated successfully!');
  2060.         // Update visualizations
  2061.         if (currentIgnitionView === '3d') {
  2062.           createIgnitionSurface();
  2063.         } else if (currentIgnitionView === 'graph') {
  2064.           initIgnitionGraph();
  2065.         }
  2066.       })
  2067.       .catch(error => console.error('Error updating ignition map:', error));
  2068.     }
  2069.  
  2070.     function autoTuneFuel() {
  2071.       if (confirm('Auto-tune will adjust fuel map based on current AFR readings. Continue?')) {
  2072.         fetch('/autotune/fuel')
  2073.           .then(() => {
  2074.             alert('Fuel auto-tune completed!');
  2075.             loadFuelMapFromECU();
  2076.           })
  2077.           .catch(error => console.error('Error auto-tuning fuel:', error));
  2078.       }
  2079.     }
  2080.    
  2081.     function autoTuneIgnition() {
  2082.       if (confirm('Auto-tune will adjust ignition map based on current engine knock detection. Continue?')) {
  2083.         fetch('/autotune/ignition')
  2084.           .then(() => {
  2085.             alert('Ignition auto-tune completed!');
  2086.             loadIgnitionMapFromECU();
  2087.           })
  2088.           .catch(error => console.error('Error auto-tuning ignition:', error));
  2089.       }
  2090.     }
  2091.  
  2092.     function smoothFuelMap() {
  2093.       if (confirm('Smooth transitions between fuel map cells?')) {
  2094.         fetch('/smoothMap/fuel')
  2095.           .then(() => {
  2096.             alert('Fuel map smoothed!');
  2097.             loadFuelMapFromECU();
  2098.           })
  2099.           .catch(error => console.error('Error smoothing fuel map:', error));
  2100.       }
  2101.     }
  2102.    
  2103.     function smoothIgnitionMap() {
  2104.       if (confirm('Smooth transitions between ignition map cells?')) {
  2105.         fetch('/smoothMap/ignition')
  2106.           .then(() => {
  2107.             alert('Ignition map smoothed!');
  2108.             loadIgnitionMapFromECU();
  2109.           })
  2110.           .catch(error => console.error('Error smoothing ignition map:', error));
  2111.       }
  2112.     }
  2113.  
  2114.     function resetFuelMap() {
  2115.       if (confirm('Reset fuel map to default values?')) {
  2116.         fetch('/resetMap/fuel')
  2117.           .then(() => {
  2118.             alert('Fuel map reset!');
  2119.             loadFuelMapFromECU();
  2120.           })
  2121.           .catch(error => console.error('Error resetting fuel map:', error));
  2122.       }
  2123.     }
  2124.    
  2125.     function resetIgnitionMap() {
  2126.       if (confirm('Reset ignition map to default values?')) {
  2127.         fetch('/resetMap/ignition')
  2128.           .then(() => {
  2129.             alert('Ignition map reset!');
  2130.             loadIgnitionMapFromECU();
  2131.           })
  2132.           .catch(error => console.error('Error resetting ignition map:', error));
  2133.       }
  2134.     }
  2135.  
  2136.     function flashFuelToECU() {
  2137.       if (confirm('Flash current fuel map to ECU?')) {
  2138.         fetch('/flashMap/fuel')
  2139.           .then(() => {
  2140.             alert('Fuel map flashed to ECU!');
  2141.           })
  2142.           .catch(error => console.error('Error flashing fuel map:', error));
  2143.       }
  2144.     }
  2145.    
  2146.     function flashIgnitionToECU() {
  2147.       if (confirm('Flash current ignition map to ECU?')) {
  2148.         fetch('/flashMap/ignition')
  2149.           .then(() => {
  2150.             alert('Ignition map flashed to ECU!');
  2151.           })
  2152.           .catch(error => console.error('Error flashing ignition map:', error));
  2153.       }
  2154.     }
  2155.  
  2156.     function addFuelRPMRow() {
  2157.       const newRPM = prompt('Enter new RPM point:');
  2158.       if (newRPM && !isNaN(newRPM)) {
  2159.         const rpm = parseInt(newRPM);
  2160.         if (rpm >= 0 && rpm <= 12000) {
  2161.           fetch('/addRPM/fuel', {
  2162.             method: 'POST',
  2163.             headers: { 'Content-Type': 'application/json' },
  2164.             body: JSON.stringify({ rpm: rpm })
  2165.           })
  2166.           .then(() => {
  2167.             loadFuelMapFromECU();
  2168.           })
  2169.           .catch(error => console.error('Error adding fuel RPM point:', error));
  2170.         }
  2171.       }
  2172.     }
  2173.    
  2174.     function addIgnitionRPMRow() {
  2175.       const newRPM = prompt('Enter new RPM point:');
  2176.       if (newRPM && !isNaN(newRPM)) {
  2177.         const rpm = parseInt(newRPM);
  2178.         if (rpm >= 0 && rpm <= 12000) {
  2179.           fetch('/addRPM/ignition', {
  2180.             method: 'POST',
  2181.             headers: { 'Content-Type': 'application/json' },
  2182.             body: JSON.stringify({ rpm: rpm })
  2183.           })
  2184.           .then(() => {
  2185.             loadIgnitionMapFromECU();
  2186.           })
  2187.           .catch(error => console.error('Error adding ignition RPM point:', error));
  2188.         }
  2189.       }
  2190.     }
  2191.  
  2192.     function removeFuelRPMRow() {
  2193.       const rpm = prompt('Enter RPM point to remove:');
  2194.       if (rpm && !isNaN(rpm)) {
  2195.         fetch('/removeRPM/fuel', {
  2196.           method: 'POST',
  2197.           headers: { 'Content-Type': 'application/json' },
  2198.           body: JSON.stringify({ rpm: parseInt(rpm) })
  2199.         })
  2200.         .then(() => {
  2201.           loadFuelMapFromECU();
  2202.         })
  2203.         .catch(error => console.error('Error removing fuel RPM point:', error));
  2204.       }
  2205.     }
  2206.    
  2207.     function removeIgnitionRPMRow() {
  2208.       const rpm = prompt('Enter RPM point to remove:');
  2209.       if (rpm && !isNaN(rpm)) {
  2210.         fetch('/removeRPM/ignition', {
  2211.           method: 'POST',
  2212.           headers: { 'Content-Type': 'application/json' },
  2213.           body: JSON.stringify({ rpm: parseInt(rpm) })
  2214.         })
  2215.         .then(() => {
  2216.           loadIgnitionMapFromECU();
  2217.         })
  2218.         .catch(error => console.error('Error removing ignition RPM point:', error));
  2219.       }
  2220.     }
  2221.  
  2222.     function exportFuelMap() {
  2223.       const mapData = JSON.stringify(fuelMapData, null, 2);
  2224.       const blob = new Blob([mapData], { type: 'application/json' });
  2225.       const url = URL.createObjectURL(blob);
  2226.       const a = document.createElement('a');
  2227.       a.href = url;
  2228.       a.download = 'fuel_map.json';
  2229.       a.click();
  2230.       URL.revokeObjectURL(url);
  2231.     }
  2232.    
  2233.     function exportIgnitionMap() {
  2234.       const mapData = JSON.stringify(ignitionMapData, null, 2);
  2235.       const blob = new Blob([mapData], { type: 'application/json' });
  2236.       const url = URL.createObjectURL(blob);
  2237.       const a = document.createElement('a');
  2238.       a.href = url;
  2239.       a.download = 'ignition_map.json';
  2240.       a.click();
  2241.       URL.revokeObjectURL(url);
  2242.     }
  2243.  
  2244.     function exportSingleFuelMap(mapId) {
  2245.       const mapData = JSON.stringify(fuelMaps[mapId].data, null, 2);
  2246.       const blob = new Blob([mapData], { type: 'application/json' });
  2247.       const url = URL.createObjectURL(blob);
  2248.       const a = document.createElement('a');
  2249.       a.href = url;
  2250.       a.download = `${fuelMaps[mapId].name.replace(/\s+/g, '_')}.json`;
  2251.       a.click();
  2252.       URL.revokeObjectURL(url);
  2253.     }
  2254.    
  2255.     function exportSingleIgnitionMap(mapId) {
  2256.       const mapData = JSON.stringify(ignitionMaps[mapId].data, null, 2);
  2257.       const blob = new Blob([mapData], { type: 'application/json' });
  2258.       const url = URL.createObjectURL(blob);
  2259.       const a = document.createElement('a');
  2260.       a.href = url;
  2261.       a.download = `${ignitionMaps[mapId].name.replace(/\s+/g, '_')}.json`;
  2262.       a.click();
  2263.       URL.revokeObjectURL(url);
  2264.     }
  2265.  
  2266.     function importFuelMap() {
  2267.       const input = document.createElement('input');
  2268.       input.type = 'file';
  2269.       input.accept = '.json';
  2270.       input.onchange = e => {
  2271.         const file = e.target.files[0];
  2272.         const reader = new FileReader();
  2273.         reader.onload = event => {
  2274.           try {
  2275.             const importedData = JSON.parse(event.target.result);
  2276.             fuelMapData = importedData;
  2277.             renderFuelMap();
  2278.             if (currentFuelView === '3d') {
  2279.               createFuelSurface();
  2280.             } else if (currentFuelView === 'graph') {
  2281.               initFuelGraph();
  2282.             }
  2283.             alert('Fuel map imported successfully!');
  2284.           } catch (error) {
  2285.             alert('Error importing fuel map: ' + error.message);
  2286.           }
  2287.         };
  2288.         reader.readAsText(file);
  2289.       };
  2290.       input.click();
  2291.     }
  2292.    
  2293.     function importIgnitionMap() {
  2294.       const input = document.createElement('input');
  2295.       input.type = 'file';
  2296.       input.accept = '.json';
  2297.       input.onchange = e => {
  2298.         const file = e.target.files[0];
  2299.         const reader = new FileReader();
  2300.         reader.onload = event => {
  2301.           try {
  2302.             const importedData = JSON.parse(event.target.result);
  2303.             ignitionMapData = importedData;
  2304.             renderIgnitionMap();
  2305.             if (currentIgnitionView === '3d') {
  2306.               createIgnitionSurface();
  2307.             } else if (currentIgnitionView === 'graph') {
  2308.               initIgnitionGraph();
  2309.             }
  2310.             alert('Ignition map imported successfully!');
  2311.           } catch (error) {
  2312.             alert('Error importing ignition map: ' + error.message);
  2313.           }
  2314.         };
  2315.         reader.readAsText(file);
  2316.       };
  2317.       input.click();
  2318.     }
  2319.  
  2320.     function saveCurrentFuelMap() {
  2321.       updateFuelMap();
  2322.       alert(`"${fuelMaps[currentFuelMapId].name}" saved successfully!`);
  2323.     }
  2324.    
  2325.     function saveCurrentIgnitionMap() {
  2326.       updateIgnitionMap();
  2327.       alert(`"${ignitionMaps[currentIgnitionMapId].name}" saved successfully!`);
  2328.     }
  2329.  
  2330.     // Lighting control functions
  2331.     function toggleHeadlights() {
  2332.       fetch('/lights/headlight/toggle')
  2333.         .then(updateSensorData)
  2334.         .catch(error => console.error('Error toggling headlights:', error));
  2335.     }
  2336.  
  2337.     function toggleHighbeams() {
  2338.       fetch('/lights/highbeam/toggle')
  2339.         .then(updateSensorData)
  2340.         .catch(error => console.error('Error toggling highbeams:', error));
  2341.     }
  2342.  
  2343.     function toggleLeftTurn() {
  2344.       fetch('/lights/leftturn/toggle')
  2345.         .then(updateSensorData)
  2346.         .catch(error => console.error('Error toggling left turn:', error));
  2347.     }
  2348.  
  2349.     function toggleRightTurn() {
  2350.       fetch('/lights/rightturn/toggle')
  2351.         .then(updateSensorData)
  2352.         .catch(error => console.error('Error toggling right turn:', error));
  2353.     }
  2354.  
  2355.     function toggleBrakeLights() {
  2356.       fetch('/lights/brake/toggle')
  2357.         .then(updateSensorData)
  2358.         .catch(error => console.error('Error toggling brake lights:', error));
  2359.     }
  2360.  
  2361.     function toggleHazards() {
  2362.       fetch('/lights/hazard/toggle')
  2363.         .then(updateSensorData)
  2364.         .catch(error => console.error('Error toggling hazards:', error));
  2365.     }
  2366.  
  2367.     // Existing sensor data and control functions
  2368.     function updateSensorData() {
  2369.       fetch('/sensorData')
  2370.         .then(response => response.json())
  2371.         .then(data => {
  2372.           // Update sensor values
  2373.           document.getElementById('rpmValue').textContent = Math.round(data.rpm);
  2374.           document.getElementById('o2Value').textContent = data.o2.toFixed(2);
  2375.           document.getElementById('tpsValue').textContent = Math.round(data.tps);
  2376.           document.getElementById('mapValue').textContent = Math.round(data.map);
  2377.           document.getElementById('tempValue').textContent = Math.round(data.temp);
  2378.           document.getElementById('voltageValue').textContent = data.voltage.toFixed(1);
  2379.           document.getElementById('activeCylinder').textContent = data.activeCylinder + 1;
  2380.           document.getElementById('fuelType').textContent = data.fuelType;
  2381.           document.getElementById('currentTune').textContent = data.currentTune;
  2382.  
  2383.           // Update cylinder pulses and timing
  2384.           document.getElementById('pulse1').textContent = data.pulseWidths[0];
  2385.           document.getElementById('pulse2').textContent = data.pulseWidths[1];
  2386.           document.getElementById('pulse3').textContent = data.pulseWidths[2];
  2387.           document.getElementById('pulse4').textContent = data.pulseWidths[3];
  2388.          
  2389.           document.getElementById('timing1').textContent = data.ignitionTiming[0];
  2390.           document.getElementById('timing2').textContent = data.ignitionTiming[1];
  2391.           document.getElementById('timing3').textContent = data.ignitionTiming[2];
  2392.           document.getElementById('timing4').textContent = data.ignitionTiming[3];
  2393.  
  2394.           // Update cylinder highlighting
  2395.           for (let i = 1; i <= 4; i++) {
  2396.             const cylElement = document.getElementById('cyl' + i);
  2397.             if (i === data.activeCylinder + 1) {
  2398.               cylElement.classList.add('cylinder-active');
  2399.             } else {
  2400.               cylElement.classList.remove('cylinder-active');
  2401.             }
  2402.           }
  2403.  
  2404.           // Update gauges
  2405.           document.getElementById('rpmGauge').style.width = Math.min(data.rpm / 100, 100) + '%';
  2406.           document.getElementById('tpsGauge').style.width = data.tps + '%';
  2407.           document.getElementById('mapGauge').style.width = data.map + '%';
  2408.           document.getElementById('tempGauge').style.width = Math.min(data.temp, 100) + '%';
  2409.           document.getElementById('voltageGauge').style.width = Math.min((data.voltage - 10) * 10, 100) + '%';
  2410.           document.getElementById('o2Gauge').style.width = ((data.o2 - 6) * 8.33) + '%';
  2411.          
  2412.           // Update system status
  2413.           const systemStatus = document.getElementById('systemStatusText');
  2414.           const systemLight = document.getElementById('systemStatusLight');
  2415.           const powerStatus = document.getElementById('powerStatus');
  2416.           const powerLight = document.getElementById('powerLight');
  2417.           const faultPanel = document.getElementById('faultPanel');
  2418.           const faultMessage = document.getElementById('faultMessage');
  2419.          
  2420.           if (data.criticalFault) {
  2421.             systemStatus.textContent = 'FAULT';
  2422.             systemStatus.style.color = '#e74c3c';
  2423.             systemLight.className = 'status-indicator status-fault';
  2424.             powerStatus.textContent = 'OFF';
  2425.             powerLight.className = 'status-indicator status-off';
  2426.             faultPanel.style.display = 'block';
  2427.             faultMessage.textContent = data.faultMessage;
  2428.           } else if (data.systemEnabled) {
  2429.             systemStatus.textContent = 'RUNNING';
  2430.             systemStatus.style.color = '#27ae60';
  2431.             systemLight.className = 'status-indicator status-on';
  2432.             powerStatus.textContent = 'ON';
  2433.             powerLight.className = 'status-indicator status-on';
  2434.             faultPanel.style.display = 'none';
  2435.           } else {
  2436.             systemStatus.textContent = 'READY';
  2437.             systemStatus.style.color = '#f39c12';
  2438.             systemLight.className = 'status-indicator status-off';
  2439.             powerStatus.textContent = 'OFF';
  2440.             powerLight.className = 'status-indicator status-off';
  2441.             faultPanel.style.display = 'none';
  2442.           }
  2443.          
  2444.           // Update lighting status
  2445.           document.getElementById('headlightStatus').textContent = data.headlightState ? 'ON' : 'OFF';
  2446.           document.getElementById('headlightStatus').className = `light-status ${data.headlightState ? 'status-on' : 'status-off'}`;
  2447.          
  2448.           document.getElementById('highbeamStatus').textContent = data.highbeamState ? 'ON' : 'OFF';
  2449.           document.getElementById('highbeamStatus').className = `light-status ${data.highbeamState ? 'status-on' : 'status-off'}`;
  2450.          
  2451.           document.getElementById('leftTurnStatus').textContent = data.leftTurnState ? 'BLINKING' : 'OFF';
  2452.           document.getElementById('leftTurnStatus').className = `light-status ${data.leftTurnState ? 'status-blinking' : 'status-off'}`;
  2453.          
  2454.           document.getElementById('rightTurnStatus').textContent = data.rightTurnState ? 'BLINKING' : 'OFF';
  2455.           document.getElementById('rightTurnStatus').className = `light-status ${data.rightTurnState ? 'status-blinking' : 'status-off'}`;
  2456.          
  2457.           document.getElementById('brakeLightStatus').textContent = data.brakeLightState ? 'ON' : 'OFF';
  2458.           document.getElementById('brakeLightStatus').className = `light-status ${data.brakeLightState ? 'status-on' : 'status-off'}`;
  2459.          
  2460.           document.getElementById('hazardStatus').textContent = data.hazardState ? 'BLINKING' : 'OFF';
  2461.           document.getElementById('hazardStatus').className = `light-status ${data.hazardState ? 'status-blinking' : 'status-off'}`;
  2462.          
  2463.           document.getElementById('pumpStatus').textContent = data.fuelPumpRunning ? 'ON' : 'OFF';
  2464.          
  2465.           // Update tuning sliders
  2466.           document.getElementById('fuelMultiplier').value = data.fuelMultiplier;
  2467.           document.getElementById('fuelMultiplierValue').textContent = data.fuelMultiplier.toFixed(2);
  2468.           document.getElementById('ignitionAdvance').value = data.ignitionAdvance;
  2469.           document.getElementById('ignitionAdvanceValue').textContent = data.ignitionAdvance.toFixed(1);
  2470.           document.getElementById('targetAFR').value = data.targetAFR;
  2471.           document.getElementById('targetAFRValue').textContent = data.targetAFR.toFixed(1);
  2472.           document.getElementById('rpmLimit').value = data.rpmLimit;
  2473.           document.getElementById('rpmLimitValue').textContent = data.rpmLimit;
  2474.         })
  2475.         .catch(error => console.error('Error fetching sensor data:', error));
  2476.     }
  2477.  
  2478.     // Control functions
  2479.     function toggleSystem() {
  2480.       fetch('/system/toggle')
  2481.         .then(updateSensorData)
  2482.         .catch(error => console.error('Error toggling system:', error));
  2483.     }
  2484.    
  2485.     function toggleFuelPump() {
  2486.       fetch('/pump/toggle')
  2487.         .then(updateSensorData)
  2488.         .catch(error => console.error('Error toggling fuel pump:', error));
  2489.     }
  2490.  
  2491.     // Preset tune functions
  2492.     function applyPreset(tuneName) {
  2493.       fetch('/preset/' + tuneName)
  2494.         .then(() => {
  2495.           const tuneInfo = {
  2496.             'economy': {name: 'Economy', desc: 'Fuel-efficient tuning for maximum mileage'},
  2497.             'performance': {name: 'Performance', desc: 'Balanced performance for street use'},
  2498.             'race': {name: 'Race', desc: 'Aggressive tuning for maximum power'},
  2499.             'methanol': {name: 'Methanol', desc: 'Optimized for methanol fuel (requires ~40% more fuel)'},
  2500.             'gasoline': {name: 'Gasoline', desc: 'Standard tuning for gasoline fuel'}
  2501.           };
  2502.          
  2503.           document.getElementById('activeTuneName').textContent = tuneInfo[tuneName].name;
  2504.           document.getElementById('tuneDescription').textContent = tuneInfo[tuneName].desc;
  2505.          
  2506.           updateSensorData();
  2507.         })
  2508.         .catch(error => console.error('Error applying preset:', error));
  2509.     }
  2510.  
  2511.     // Tuning functions
  2512.     function updateFuelMultiplier(value) {
  2513.       document.getElementById('fuelMultiplierValue').textContent = parseFloat(value).toFixed(2);
  2514.       fetch('/tuning/fuelMultiplier?value=' + value)
  2515.         .catch(error => console.error('Error updating fuel multiplier:', error));
  2516.     }
  2517.    
  2518.     function updateIgnitionAdvance(value) {
  2519.       document.getElementById('ignitionAdvanceValue').textContent = parseFloat(value).toFixed(1);
  2520.       fetch('/tuning/ignitionAdvance?value=' + value)
  2521.         .catch(error => console.error('Error updating ignition advance:', error));
  2522.     }
  2523.    
  2524.     function updateTargetAFR(value) {
  2525.       document.getElementById('targetAFRValue').textContent = parseFloat(value).toFixed(1);
  2526.       fetch('/tuning/targetAFR?value=' + value)
  2527.         .catch(error => console.error('Error updating target AFR:', error));
  2528.     }
  2529.    
  2530.     function updateRPMLimit(value) {
  2531.       document.getElementById('rpmLimitValue').textContent = value;
  2532.       fetch('/tuning/rpmLimit?value=' + value)
  2533.         .then(() => {
  2534.           // Reload maps when RPM limit changes
  2535.           loadFuelMapFromECU();
  2536.           loadIgnitionMapFromECU();
  2537.         })
  2538.         .catch(error => console.error('Error updating RPM limit:', error));
  2539.     }
  2540.    
  2541.     function saveTuning() {
  2542.       fetch('/tuning/save')
  2543.         .then(() => alert('Tuning parameters saved!'))
  2544.         .catch(error => console.error('Error saving tuning:', error));
  2545.     }
  2546.    
  2547.     function loadTuning() {
  2548.       fetch('/tuning/load')
  2549.         .then(() => {
  2550.           alert('Tuning parameters loaded!');
  2551.           location.reload();
  2552.         })
  2553.         .catch(error => console.error('Error loading tuning:', error));
  2554.     }
  2555.    
  2556.     function resetTuning() {
  2557.       if (confirm('Reset all tuning to defaults?')) {
  2558.         fetch('/tuning/reset')
  2559.           .then(() => {
  2560.             alert('Tuning parameters reset!');
  2561.             location.reload();
  2562.           })
  2563.           .catch(error => console.error('Error resetting tuning:', error));
  2564.       }
  2565.     }
  2566.  
  2567.     // Initialize everything when page loads
  2568.     document.addEventListener('DOMContentLoaded', function() {
  2569.       initializeFuelMap();
  2570.       initializeIgnitionMap();
  2571.       loadFuelMapSelection();
  2572.       loadIgnitionMapSelection();
  2573.       selectFuelMap('base');
  2574.       selectIgnitionMap('base');
  2575.       setInterval(updateSensorData, 500);
  2576.      
  2577.       // Set initial tune info
  2578.       document.getElementById('activeTuneName').textContent = 'Performance';
  2579.       document.getElementById('tuneDescription').textContent = 'Balanced performance for street use';
  2580.     });
  2581.   </script>
  2582. </body>
  2583. </html>
  2584. )rawliteral";
  2585.  
  2586. // ==================== FUEL & IGNITION MAP MANAGEMENT ====================
  2587. void initializeFuelMap() {
  2588.  fuelMap.name = "Base Fuel Map";
  2589.  fuelMap.description = "Default 4-cylinder fuel map";
  2590.  fuelMap.fuelType = "gasoline";
  2591.  
  2592.  // Initialize RPM points from 0 to limit in steps
  2593.  fuelMap.rpmPointCount = 0;
  2594.  for (int rpm = tuning.minRPM; rpm <= tuning.maxRPM; rpm += tuning.rpmStep) {
  2595.    if (fuelMap.rpmPointCount < MAX_RPM_POINTS) {
  2596.      fuelMap.rpmPoints[fuelMap.rpmPointCount] = rpm;
  2597.      fuelMap.rpmPointCount++;
  2598.    }
  2599.  }
  2600.  
  2601.  // Initialize load points from 0 to 100% in steps
  2602.  fuelMap.loadPointCount = 0;
  2603.  for (int load = 0; load <= 100; load += tuning.loadStep) {
  2604.    if (fuelMap.loadPointCount < MAX_LOAD_POINTS) {
  2605.      fuelMap.loadPoints[fuelMap.loadPointCount] = load;
  2606.      fuelMap.loadPointCount++;
  2607.    }
  2608.  }
  2609.  
  2610.  // Initialize all cells with default values
  2611.  for (int i = 0; i < fuelMap.rpmPointCount; i++) {
  2612.    for (int j = 0; j < fuelMap.loadPointCount; j++) {
  2613.      int rpm = fuelMap.rpmPoints[i];
  2614.      int load = fuelMap.loadPoints[j];
  2615.      
  2616.      // Base pulse width calculation
  2617.      int basePulse = 800 + (rpm * 0.1) + (load * 8);
  2618.      fuelMap.cells[i][j].pulseWidth = basePulse;
  2619.      fuelMap.cells[i][j].learned = false;
  2620.      fuelMap.cells[i][j].correction = 1.0;
  2621.    }
  2622.  }
  2623.  
  2624.  // Initialize multiple fuel maps
  2625.  fuelMapCount = 3;
  2626.  
  2627.  // Base map
  2628.  fuelMaps[0] = fuelMap;
  2629.  
  2630.  // Economy map
  2631.  fuelMaps[1] = fuelMap;
  2632.  fuelMaps[1].name = "Economy";
  2633.  fuelMaps[1].description = "Fuel efficiency focused map";
  2634.  for (int i = 0; i < fuelMaps[1].rpmPointCount; i++) {
  2635.    for (int j = 0; j < fuelMaps[1].loadPointCount; j++) {
  2636.      fuelMaps[1].cells[i][j].pulseWidth *= 0.9; // 10% leaner
  2637.    }
  2638.  }
  2639.  
  2640.  // Race map
  2641.  fuelMaps[2] = fuelMap;
  2642.  fuelMaps[2].name = "Race";
  2643.  fuelMaps[2].description = "Aggressive race tuning";
  2644.  for (int i = 0; i < fuelMaps[2].rpmPointCount; i++) {
  2645.    for (int j = 0; j < fuelMaps[2].loadPointCount; j++) {
  2646.      fuelMaps[2].cells[i][j].pulseWidth *= 1.15; // 15% richer
  2647.    }
  2648.  }
  2649. }
  2650.  
  2651. void initializeIgnitionMap() {
  2652.  ignitionMap.name = "Base Ignition Map";
  2653.  ignitionMap.description = "Default 4-cylinder ignition map";
  2654.  
  2655.  // Initialize RPM points from 0 to limit in steps
  2656.  ignitionMap.rpmPointCount = 0;
  2657.  for (int rpm = tuning.minRPM; rpm <= tuning.maxRPM; rpm += tuning.rpmStep) {
  2658.    if (ignitionMap.rpmPointCount < MAX_RPM_POINTS) {
  2659.      ignitionMap.rpmPoints[ignitionMap.rpmPointCount] = rpm;
  2660.      ignitionMap.rpmPointCount++;
  2661.    }
  2662.  }
  2663.  
  2664.  // Initialize load points from 0 to 100% in steps
  2665.  ignitionMap.loadPointCount = 0;
  2666.  for (int load = 0; load <= 100; load += tuning.loadStep) {
  2667.    if (ignitionMap.loadPointCount < MAX_LOAD_POINTS) {
  2668.      ignitionMap.loadPoints[ignitionMap.loadPointCount] = load;
  2669.      ignitionMap.loadPointCount++;
  2670.    }
  2671.  }
  2672.  
  2673.  // Initialize all cells with default values
  2674.  for (int i = 0; i < ignitionMap.rpmPointCount; i++) {
  2675.    for (int j = 0; j < ignitionMap.loadPointCount; j++) {
  2676.      int rpm = ignitionMap.rpmPoints[i];
  2677.      int load = ignitionMap.loadPoints[j];
  2678.      
  2679.      // Base ignition advance calculation
  2680.      float baseAdvance = 10.0 + (rpm * 0.001) + (load * 0.1);
  2681.      ignitionMap.cells[i][j].advance = baseAdvance;
  2682.      ignitionMap.cells[i][j].learned = false;
  2683.      ignitionMap.cells[i][j].correction = 1.0;
  2684.    }
  2685.  }
  2686.  
  2687.  // Initialize multiple ignition maps
  2688.  ignitionMapCount = 3;
  2689.  
  2690.  // Base map
  2691.  ignitionMaps[0] = ignitionMap;
  2692.  
  2693.  // Economy map
  2694.  ignitionMaps[1] = ignitionMap;
  2695.  ignitionMaps[1].name = "Economy";
  2696.  ignitionMaps[1].description = "Fuel efficiency focused ignition";
  2697.  for (int i = 0; i < ignitionMaps[1].rpmPointCount; i++) {
  2698.    for (int j = 0; j < ignitionMaps[1].loadPointCount; j++) {
  2699.      ignitionMaps[1].cells[i][j].advance += 2.0; // More advance for economy
  2700.    }
  2701.  }
  2702.  
  2703.  // Race map
  2704.  ignitionMaps[2] = ignitionMap;
  2705.  ignitionMaps[2].name = "Race";
  2706.  ignitionMaps[2].description = "Aggressive race ignition";
  2707.  for (int i = 0; i < ignitionMaps[2].rpmPointCount; i++) {
  2708.    for (int j = 0; j < ignitionMaps[2].loadPointCount; j++) {
  2709.      ignitionMaps[2].cells[i][j].advance -= 2.0; // Less advance for race (conservative)
  2710.    }
  2711.  }
  2712. }
  2713.  
  2714. int findRPMIndex(int rpm, bool isFuelMap = true) {
  2715.  if (isFuelMap) {
  2716.    for (int i = 0; i < fuelMap.rpmPointCount; i++) {
  2717.      if (fuelMap.rpmPoints[i] >= rpm) {
  2718.        return i;
  2719.      }
  2720.    }
  2721.    return fuelMap.rpmPointCount - 1;
  2722.  } else {
  2723.    for (int i = 0; i < ignitionMap.rpmPointCount; i++) {
  2724.      if (ignitionMap.rpmPoints[i] >= rpm) {
  2725.        return i;
  2726.      }
  2727.    }
  2728.    return ignitionMap.rpmPointCount - 1;
  2729.  }
  2730. }
  2731.  
  2732. int findLoadIndex(int load, bool isFuelMap = true) {
  2733.  if (isFuelMap) {
  2734.    for (int i = 0; i < fuelMap.loadPointCount; i++) {
  2735.      if (fuelMap.loadPoints[i] >= load) {
  2736.        return i;
  2737.      }
  2738.    }
  2739.    return fuelMap.loadPointCount - 1;
  2740.  } else {
  2741.    for (int i = 0; i < ignitionMap.loadPointCount; i++) {
  2742.      if (ignitionMap.loadPoints[i] >= load) {
  2743.        return i;
  2744.      }
  2745.    }
  2746.    return ignitionMap.loadPointCount - 1;
  2747.  }
  2748. }
  2749.  
  2750. int getFuelPulseWidth(int rpm, int load) {
  2751.  if (rpm < tuning.minRPM) rpm = tuning.minRPM;
  2752.  if (rpm > tuning.maxRPM) rpm = tuning.maxRPM;
  2753.  if (load < 0) load = 0;
  2754.  if (load > 100) load = 100;
  2755.  
  2756.  int rpmIndex = findRPMIndex(rpm, true);
  2757.  int loadIndex = findLoadIndex(load, true);
  2758.  
  2759.  int basePulse = fuelMap.cells[rpmIndex][loadIndex].pulseWidth;
  2760.  float correction = fuelMap.cells[rpmIndex][loadIndex].correction;
  2761.  
  2762.  // Apply tuning adjustments
  2763.  float adjustedPulse = basePulse * tuning.fuelMultiplier * correction;
  2764.  
  2765.  // Closed-loop correction based on O2 sensor
  2766.  float afrError = o2Value - tuning.targetAFR;
  2767.  float closedLoopCorrection = 1.0 + (afrError * 0.05);
  2768.  closedLoopCorrection = constrain(closedLoopCorrection, 0.7, 1.3);
  2769.  
  2770.  adjustedPulse *= closedLoopCorrection;
  2771.  
  2772.  return (int)adjustedPulse;
  2773. }
  2774.  
  2775. float getIgnitionAdvance(int rpm, int load) {
  2776.  if (rpm < tuning.minRPM) rpm = tuning.minRPM;
  2777.  if (rpm > tuning.maxRPM) rpm = tuning.maxRPM;
  2778.  if (load < 0) load = 0;
  2779.  if (load > 100) load = 100;
  2780.  
  2781.  int rpmIndex = findRPMIndex(rpm, false);
  2782.  int loadIndex = findLoadIndex(load, false);
  2783.  
  2784.  float baseAdvance = ignitionMap.cells[rpmIndex][loadIndex].advance;
  2785.  float correction = ignitionMap.cells[rpmIndex][loadIndex].correction;
  2786.  
  2787.  // Apply tuning adjustments
  2788.  float adjustedAdvance = baseAdvance + (tuning.ignitionAdvance - 12.0);
  2789.  adjustedAdvance *= correction;
  2790.  
  2791.  // Safety limits
  2792.  adjustedAdvance = constrain(adjustedAdvance, 5.0, 35.0);
  2793.  
  2794.  return adjustedAdvance;
  2795. }
  2796.  
  2797. // ==================== EEPROM MANAGEMENT ====================
  2798. void saveTuningToEEPROM() {
  2799.  EEPROM.begin(8192);
  2800.  int addr = 0;
  2801.  
  2802.  // Save tuning parameters
  2803.  EEPROM.put(addr, tuning);
  2804.  addr += sizeof(TuningParams);
  2805.  
  2806.  // Save fuel map
  2807.  EEPROM.put(addr, fuelMap);
  2808.  addr += sizeof(FuelMap);
  2809.  
  2810.  // Save ignition map
  2811.  EEPROM.put(addr, ignitionMap);
  2812.  addr += sizeof(IgnitionMap);
  2813.  
  2814.  // Save multiple fuel maps
  2815.  EEPROM.put(addr, fuelMapCount);
  2816.  addr += sizeof(int);
  2817.  
  2818.  for (int i = 0; i < fuelMapCount; i++) {
  2819.    EEPROM.put(addr, fuelMaps[i]);
  2820.    addr += sizeof(FuelMap);
  2821.  }
  2822.  
  2823.  // Save multiple ignition maps
  2824.  EEPROM.put(addr, ignitionMapCount);
  2825.  addr += sizeof(int);
  2826.  
  2827.  for (int i = 0; i < ignitionMapCount; i++) {
  2828.    EEPROM.put(addr, ignitionMaps[i]);
  2829.    addr += sizeof(IgnitionMap);
  2830.  }
  2831.  
  2832.  EEPROM.put(addr, currentFuelMapIndex);
  2833.  addr += sizeof(int);
  2834.  
  2835.  EEPROM.put(addr, currentIgnitionMapIndex);
  2836.  
  2837.  EEPROM.commit();
  2838.  EEPROM.end();
  2839.  Serial.println("Tuning saved to EEPROM");
  2840. }
  2841.  
  2842. void loadTuningFromEEPROM() {
  2843.  EEPROM.begin(8192);
  2844.  int addr = 0;
  2845.  
  2846.  // Load tuning parameters
  2847.  EEPROM.get(addr, tuning);
  2848.  addr += sizeof(TuningParams);
  2849.  
  2850.  // Load fuel map
  2851.  EEPROM.get(addr, fuelMap);
  2852.  addr += sizeof(FuelMap);
  2853.  
  2854.  // Load ignition map
  2855.  EEPROM.get(addr, ignitionMap);
  2856.  addr += sizeof(IgnitionMap);
  2857.  
  2858.  // Load multiple fuel maps
  2859.  EEPROM.get(addr, fuelMapCount);
  2860.  addr += sizeof(int);
  2861.  
  2862.  for (int i = 0; i < fuelMapCount && i < MAX_FUEL_MAPS; i++) {
  2863.    EEPROM.get(addr, fuelMaps[i]);
  2864.    addr += sizeof(FuelMap);
  2865.  }
  2866.  
  2867.  // Load multiple ignition maps
  2868.  EEPROM.get(addr, ignitionMapCount);
  2869.  addr += sizeof(int);
  2870.  
  2871.  for (int i = 0; i < ignitionMapCount && i < MAX_IGNITION_MAPS; i++) {
  2872.    EEPROM.get(addr, ignitionMaps[i]);
  2873.    addr += sizeof(IgnitionMap);
  2874.  }
  2875.  
  2876.  EEPROM.get(addr, currentFuelMapIndex);
  2877.  addr += sizeof(int);
  2878.  
  2879.  EEPROM.get(addr, currentIgnitionMapIndex);
  2880.  
  2881.  EEPROM.end();
  2882.  Serial.println("Tuning loaded from EEPROM");
  2883. }
  2884.  
  2885. // ==================== LIGHTING CONTROL ====================
  2886. void updateLighting() {
  2887.  // Headlights
  2888.  digitalWrite(PIN_HEADLIGHT, headlightState);
  2889.  digitalWrite(PIN_HIGHBEAM, highbeamState && headlightState);
  2890.  
  2891.  // Brake lights
  2892.  digitalWrite(PIN_BRAKE_LIGHT, brakeLightState);
  2893.  
  2894.  // Turn signals with hazard override
  2895.  unsigned long currentMillis = millis();
  2896.  if (currentMillis - turnSignalPreviousMillis >= turnSignalInterval) {
  2897.    turnSignalPreviousMillis = currentMillis;
  2898.    turnSignalBlinkState = !turnSignalBlinkState;
  2899.  }
  2900.  
  2901.  if (hazardState) {
  2902.    digitalWrite(PIN_LEFT_TURN, turnSignalBlinkState);
  2903.    digitalWrite(PIN_RIGHT_TURN, turnSignalBlinkState);
  2904.  } else {
  2905.    digitalWrite(PIN_LEFT_TURN, leftTurnState && turnSignalBlinkState);
  2906.    digitalWrite(PIN_RIGHT_TURN, rightTurnState && turnSignalBlinkState);
  2907.  }
  2908. }
  2909.  
  2910. void toggleHeadlight() {
  2911.  headlightState = !headlightState;
  2912.  if (!headlightState) highbeamState = false;
  2913. }
  2914.  
  2915. void toggleHighbeam() {
  2916.  if (headlightState) {
  2917.    highbeamState = !highbeamState;
  2918.  }
  2919. }
  2920.  
  2921. void toggleLeftTurn() {
  2922.  leftTurnState = !leftTurnState;
  2923.  if (leftTurnState) rightTurnState = false;
  2924. }
  2925.  
  2926. void toggleRightTurn() {
  2927.  rightTurnState = !rightTurnState;
  2928.  if (rightTurnState) leftTurnState = false;
  2929. }
  2930.  
  2931. void toggleBrakeLight() {
  2932.  brakeLightState = !brakeLightState;
  2933. }
  2934.  
  2935. void toggleHazard() {
  2936.  hazardState = !hazardState;
  2937. }
  2938.  
  2939. // ==================== 4-CYLINDER ENGINE CONTROL ====================
  2940. void IRAM_ATTR rpmInterrupt() {
  2941.  unsigned long currentTime = micros();
  2942.  rpmPulseTime = currentTime - lastRpmPulse;
  2943.  lastRpmPulse = currentTime;
  2944.  
  2945.  if (rpmPulseTime > 500) {
  2946.    currentRPM = 60000000.0 / rpmPulseTime;
  2947.    if (currentRPM > 20000) currentRPM = 0;
  2948.    
  2949.    // Advance to next cylinder (1-3-4-2 firing order)
  2950.    currentCylinder = (currentCylinder + 1) % 4;
  2951.    lastCylinderTime = currentTime;
  2952.  }
  2953. }
  2954.  
  2955. void controlInjection() {
  2956.  if (!systemEnabled || currentRPM == 0) {
  2957.    for (int i = 0; i < 4; i++) {
  2958.      digitalWrite(PIN_INJECTOR_1 + i, LOW);
  2959.    }
  2960.    return;
  2961.  }
  2962.  
  2963.  unsigned long currentTime = micros();
  2964.  int pulseWidth = getFuelPulseWidth(currentRPM, mapValue);
  2965.  
  2966.  // Calculate injection timing (sequential injection)
  2967.  unsigned long injectionInterval = 120000000 / currentRPM;
  2968.  
  2969.  // Fire injector for current cylinder in firing order
  2970.  int injectorPin = PIN_INJECTOR_1 + firingOrder[currentCylinder];
  2971.  
  2972.  if (currentTime - lastInjectorPulse[currentCylinder] > injectionInterval) {
  2973.    digitalWrite(injectorPin, HIGH);
  2974.    lastInjectorPulse[currentCylinder] = currentTime;
  2975.    currentPulseWidth[currentCylinder] = pulseWidth;
  2976.  }
  2977.  
  2978.  // Turn off injector after pulse width duration
  2979.  if (digitalRead(injectorPin) == HIGH &&
  2980.      currentTime - lastInjectorPulse[currentCylinder] > currentPulseWidth[currentCylinder]) {
  2981.    digitalWrite(injectorPin, LOW);
  2982.  }
  2983. }
  2984.  
  2985. void controlIgnition() {
  2986.  if (!systemEnabled || currentRPM == 0) {
  2987.    for (int i = 0; i < 4; i++) {
  2988.      digitalWrite(PIN_IGNITION_1 + i, LOW);
  2989.    }
  2990.    return;
  2991.  }
  2992.  
  2993.  unsigned long currentTime = micros();
  2994.  unsigned long ignitionInterval = 120000000 / currentRPM;
  2995.  float ignitionAdvance = getIgnitionAdvance(currentRPM, mapValue);
  2996.  
  2997.  // Convert advance angle to time (microseconds)
  2998.  unsigned long advanceTime = (ignitionAdvance * 1000000) / (currentRPM * 6);
  2999.  unsigned long dwellTime = 2000; // 2ms dwell
  3000.  
  3001.  // Fire ignition for current cylinder
  3002.  int ignitionPin = PIN_IGNITION_1 + ignitionOrder[currentCylinder];
  3003.  
  3004.  static unsigned long lastIgnitionTime[4] = {0, 0, 0, 0};
  3005.  static float lastIgnitionTiming[4] = {0, 0, 0, 0};
  3006.  
  3007.  if (currentTime - lastIgnitionTime[currentCylinder] > ignitionInterval) {
  3008.    digitalWrite(ignitionPin, HIGH);
  3009.    lastIgnitionTime[currentCylinder] = currentTime;
  3010.    lastIgnitionTiming[currentCylinder] = ignitionAdvance;
  3011.  }
  3012.  
  3013.  if (currentTime - lastIgnitionTime[currentCylinder] > dwellTime) {
  3014.    digitalWrite(ignitionPin, LOW);
  3015.  }
  3016. }
  3017.  
  3018. // ==================== SENSOR READING ====================
  3019. void readSensors() {
  3020.  if(systemEnabled && currentRPM > 0) {
  3021.    // Simulate sensor readings
  3022.    int tpsRaw = analogRead(PIN_TPS);
  3023.    tpsValue = map(tpsRaw, 0, 4095, 0, 100);
  3024.    
  3025.    int mapRaw = analogRead(PIN_MAP_SENSOR);
  3026.    mapValue = map(tpsRaw, 0, 4095, 10, 90) + (currentRPM * 0.001);
  3027.    if(mapValue > 100) mapValue = 100;
  3028.    
  3029.    o2Value = 14.7 + (sin(millis() * 0.001) * 0.5);
  3030.    engineTemp = 80 + (sin(millis() * 0.0001) * 5);
  3031.    batteryVoltage = 12.5 + (sin(millis() * 0.0005) * 0.3);
  3032.  } else {
  3033.    o2Value = 14.7;
  3034.    tpsValue = 0;
  3035.    mapValue = 0;
  3036.    engineTemp = 20;
  3037.    batteryVoltage = 12.5;
  3038.  }
  3039.  
  3040.  if (micros() - lastRpmPulse > 1000000) {
  3041.    currentRPM = 0;
  3042.  }
  3043. }
  3044.  
  3045. // ==================== FAIL-SAFE SYSTEM ====================
  3046. bool performSafetyChecks() {
  3047.  if (currentRPM > tuning.rpmLimit) {
  3048.    faultMessage = "RPM LIMIT EXCEEDED: " + String(currentRPM);
  3049.    return false;
  3050.  }
  3051.  
  3052.  if (engineTemp > tuning.tempLimit) {
  3053.    faultMessage = "ENGINE OVERHEAT: " + String(engineTemp) + "C";
  3054.    return false;
  3055.  }
  3056.  
  3057.  if (batteryVoltage < 10.0) {
  3058.    faultMessage = "LOW BATTERY: " + String(batteryVoltage) + "V";
  3059.    return false;
  3060.  }
  3061.  
  3062.  faultMessage = "";
  3063.  return true;
  3064. }
  3065.  
  3066. void activateFailSafe() {
  3067.  criticalFault = true;
  3068.  systemEnabled = false;
  3069.  
  3070.  // Shut down all outputs
  3071.  for (int i = 0; i < 4; i++) {
  3072.    digitalWrite(PIN_INJECTOR_1 + i, LOW);
  3073.    digitalWrite(PIN_IGNITION_1 + i, LOW);
  3074.  }
  3075.  fuelPumpRunning = false;
  3076.  
  3077.  Serial.println("FAIL-SAFE ACTIVATED: " + faultMessage);
  3078. }
  3079.  
  3080. // ==================== WEB SERVER SETUP ====================
  3081. void setupWebServer() {
  3082.  // Serve main page
  3083.  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
  3084.    request->send_P(200, "text/html", index_html);
  3085.  });
  3086.  
  3087.  // Sensor data API
  3088.  server.on("/sensorData", HTTP_GET, [](AsyncWebServerRequest *request) {
  3089.    StaticJsonDocument<2048> doc;
  3090.    
  3091.    doc["rpm"] = currentRPM;
  3092.    doc["o2"] = o2Value;
  3093.    doc["tps"] = tpsValue;
  3094.    doc["map"] = mapValue;
  3095.    doc["temp"] = engineTemp;
  3096.    doc["voltage"] = batteryVoltage;
  3097.    doc["activeCylinder"] = currentCylinder;
  3098.    
  3099.    JsonArray pulseWidths = doc.createNestedArray("pulseWidths");
  3100.    for (int i = 0; i < 4; i++) {
  3101.      pulseWidths.add(currentPulseWidth[i]);
  3102.    }
  3103.    
  3104.    // Add ignition timing data
  3105.    JsonArray ignitionTiming = doc.createNestedArray("ignitionTiming");
  3106.    float ignitionAdvance = getIgnitionAdvance(currentRPM, mapValue);
  3107.    for (int i = 0; i < 4; i++) {
  3108.      ignitionTiming.add(ignitionAdvance);
  3109.    }
  3110.    
  3111.    doc["systemEnabled"] = systemEnabled;
  3112.    doc["fuelPumpRunning"] = fuelPumpRunning;
  3113.    doc["criticalFault"] = criticalFault;
  3114.    doc["faultMessage"] = faultMessage;
  3115.    doc["fuelMultiplier"] = tuning.fuelMultiplier;
  3116.    doc["ignitionAdvance"] = tuning.ignitionAdvance;
  3117.    doc["targetAFR"] = tuning.targetAFR;
  3118.    doc["rpmLimit"] = tuning.rpmLimit;
  3119.    doc["currentTune"] = tuning.currentTune;
  3120.    doc["fuelType"] = tuning.fuelType;
  3121.    
  3122.    // Lighting states
  3123.    doc["headlightState"] = headlightState;
  3124.    doc["highbeamState"] = highbeamState;
  3125.    doc["leftTurnState"] = leftTurnState;
  3126.    doc["rightTurnState"] = rightTurnState;
  3127.    doc["brakeLightState"] = brakeLightState;
  3128.    doc["hazardState"] = hazardState;
  3129.    
  3130.    String response;
  3131.    serializeJson(doc, response);
  3132.    request->send(200, "application/json", response);
  3133.  });
  3134.  
  3135.  // System control
  3136.  server.on("/system/toggle", HTTP_GET, [](AsyncWebServerRequest *request) {
  3137.    if (!criticalFault) {
  3138.      systemEnabled = !systemEnabled;
  3139.      if (systemEnabled) engineRunTime = millis();
  3140.    }
  3141.    request->send(200, "text/plain", "OK");
  3142.  });
  3143.  
  3144.  server.on("/pump/toggle", HTTP_GET, [](AsyncWebServerRequest *request) {
  3145.    if (!criticalFault) {
  3146.      fuelPumpRunning = !fuelPumpRunning;
  3147.    }
  3148.    request->send(200, "text/plain", "OK");
  3149.  });
  3150.  
  3151.  // Preset tunes
  3152.  server.on("/preset/economy", HTTP_GET, [](AsyncWebServerRequest *request) {
  3153.    tuning.fuelMultiplier = 0.9;
  3154.    tuning.ignitionAdvance = 14.0;
  3155.    tuning.targetAFR = 15.5;
  3156.    tuning.rpmLimit = 6500;
  3157.    tuning.currentTune = "economy";
  3158.    request->send(200, "text/plain", "OK");
  3159.  });
  3160.  
  3161.  server.on("/preset/performance", HTTP_GET, [](AsyncWebServerRequest *request) {
  3162.    tuning.fuelMultiplier = 1.0;
  3163.    tuning.ignitionAdvance = 12.0;
  3164.    tuning.targetAFR = 14.7;
  3165.    tuning.rpmLimit = 8000;
  3166.    tuning.currentTune = "performance";
  3167.    request->send(200, "text/plain", "OK");
  3168.  });
  3169.  
  3170.  server.on("/preset/race", HTTP_GET, [](AsyncWebServerRequest *request) {
  3171.    tuning.fuelMultiplier = 1.15;
  3172.    tuning.ignitionAdvance = 10.0;
  3173.    tuning.targetAFR = 13.5;
  3174.    tuning.rpmLimit = 9000;
  3175.    tuning.currentTune = "race";
  3176.    request->send(200, "text/plain", "OK");
  3177.  });
  3178.  
  3179.  server.on("/preset/methanol", HTTP_GET, [](AsyncWebServerRequest *request) {
  3180.    tuning.fuelMultiplier = 1.4;
  3181.    tuning.ignitionAdvance = 16.0;
  3182.    tuning.targetAFR = 6.5;
  3183.    tuning.rpmLimit = 8500;
  3184.    tuning.fuelType = "methanol";
  3185.    tuning.currentTune = "methanol";
  3186.    request->send(200, "text/plain", "OK");
  3187.  });
  3188.  
  3189.  server.on("/preset/gasoline", HTTP_GET, [](AsyncWebServerRequest *request) {
  3190.    tuning.fuelMultiplier = 1.0;
  3191.    tuning.ignitionAdvance = 12.0;
  3192.    tuning.targetAFR = 14.7;
  3193.    tuning.rpmLimit = 8000;
  3194.    tuning.fuelType = "gasoline";
  3195.    tuning.currentTune = "gasoline";
  3196.    request->send(200, "text/plain", "OK");
  3197.  });
  3198.  
  3199.  // Lighting control routes
  3200.  server.on("/lights/headlight/toggle", HTTP_GET, [](AsyncWebServerRequest *request) {
  3201.    toggleHeadlight();
  3202.    request->send(200, "text/plain", "OK");
  3203.  });
  3204.  
  3205.  server.on("/lights/highbeam/toggle", HTTP_GET, [](AsyncWebServerRequest *request) {
  3206.    toggleHighbeam();
  3207.    request->send(200, "text/plain", "OK");
  3208.  });
  3209.  
  3210.  server.on("/lights/leftturn/toggle", HTTP_GET, [](AsyncWebServerRequest *request) {
  3211.    toggleLeftTurn();
  3212.    request->send(200, "text/plain", "OK");
  3213.  });
  3214.  
  3215.  server.on("/lights/rightturn/toggle", HTTP_GET, [](AsyncWebServerRequest *request) {
  3216.    toggleRightTurn();
  3217.    request->send(200, "text/plain", "OK");
  3218.  });
  3219.  
  3220.  server.on("/lights/brake/toggle", HTTP_GET, [](AsyncWebServerRequest *request) {
  3221.    toggleBrakeLight();
  3222.    request->send(200, "text/plain", "OK");
  3223.  });
  3224.  
  3225.  server.on("/lights/hazard/toggle", HTTP_GET, [](AsyncWebServerRequest *request) {
  3226.    toggleHazard();
  3227.    request->send(200, "text/plain", "OK");
  3228.  });
  3229.  
  3230.  // Tuning parameter routes
  3231.  server.on("/tuning/fuelMultiplier", HTTP_GET, [](AsyncWebServerRequest *request) {
  3232.    if (request->hasParam("value")) {
  3233.      tuning.fuelMultiplier = request->getParam("value")->value().toFloat();
  3234.    }
  3235.    request->send(200, "text/plain", "OK");
  3236.  });
  3237.  
  3238.  server.on("/tuning/ignitionAdvance", HTTP_GET, [](AsyncWebServerRequest *request) {
  3239.    if (request->hasParam("value")) {
  3240.      tuning.ignitionAdvance = request->getParam("value")->value().toFloat();
  3241.    }
  3242.    request->send(200, "text/plain", "OK");
  3243.  });
  3244.  
  3245.  server.on("/tuning/targetAFR", HTTP_GET, [](AsyncWebServerRequest *request) {
  3246.    if (request->hasParam("value")) {
  3247.      tuning.targetAFR = request->getParam("value")->value().toFloat();
  3248.    }
  3249.    request->send(200, "text/plain", "OK");
  3250.  });
  3251.  
  3252.  server.on("/tuning/rpmLimit", HTTP_GET, [](AsyncWebServerRequest *request) {
  3253.    if (request->hasParam("value")) {
  3254.      tuning.rpmLimit = request->getParam("value")->value().toInt();
  3255.      tuning.maxRPM = tuning.rpmLimit;
  3256.      initializeFuelMap();
  3257.      initializeIgnitionMap();
  3258.    }
  3259.    request->send(200, "text/plain", "OK");
  3260.  });
  3261.  
  3262.  server.on("/tuning/save", HTTP_GET, [](AsyncWebServerRequest *request) {
  3263.    saveTuningToEEPROM();
  3264.    request->send(200, "text/plain", "OK");
  3265.  });
  3266.  
  3267.  server.on("/tuning/load", HTTP_GET, [](AsyncWebServerRequest *request) {
  3268.    loadTuningFromEEPROM();
  3269.    request->send(200, "text/plain", "OK");
  3270.  });
  3271.  
  3272.  server.on("/tuning/reset", HTTP_GET, [](AsyncWebServerRequest *request) {
  3273.    tuning.fuelMultiplier = 1.0;
  3274.    tuning.ignitionAdvance = 12.0;
  3275.    tuning.targetAFR = 14.7;
  3276.    tuning.rpmLimit = 8000;
  3277.    tuning.tempLimit = 105;
  3278.    tuning.minRPM = 0;
  3279.    tuning.maxRPM = 8000;
  3280.    tuning.rpmStep = 250;
  3281.    tuning.loadStep = 10;
  3282.    tuning.currentTune = "performance";
  3283.    tuning.fuelType = "gasoline";
  3284.    
  3285.    initializeFuelMap();
  3286.    initializeIgnitionMap();
  3287.    request->send(200, "text/plain", "OK");
  3288.  });
  3289.  
  3290.  // Fuel map API
  3291.  server.on("/fuelMap", HTTP_GET, [](AsyncWebServerRequest *request) {
  3292.    StaticJsonDocument<16384> doc;
  3293.    
  3294.    doc["name"] = fuelMap.name;
  3295.    doc["description"] = fuelMap.description;
  3296.    doc["fuelType"] = fuelMap.fuelType;
  3297.    doc["rpmLimit"] = tuning.rpmLimit;
  3298.    doc["rpmStep"] = tuning.rpmStep;
  3299.    doc["loadStep"] = tuning.loadStep;
  3300.    
  3301.    JsonArray rpmPoints = doc.createNestedArray("rpmPoints");
  3302.    for (int i = 0; i < fuelMap.rpmPointCount; i++) {
  3303.      rpmPoints.add(fuelMap.rpmPoints[i]);
  3304.    }
  3305.    
  3306.    JsonArray loadPoints = doc.createNestedArray("loadPoints");
  3307.    for (int i = 0; i < fuelMap.loadPointCount; i++) {
  3308.      loadPoints.add(fuelMap.loadPoints[i]);
  3309.    }
  3310.    
  3311.    JsonObject cells = doc.createNestedObject("cells");
  3312.    for (int i = 0; i < fuelMap.rpmPointCount; i++) {
  3313.      JsonObject rpmCell = cells.createNestedObject(String(fuelMap.rpmPoints[i]));
  3314.      for (int j = 0; j < fuelMap.loadPointCount; j++) {
  3315.        rpmCell[String(fuelMap.loadPoints[j])] = fuelMap.cells[i][j].pulseWidth;
  3316.      }
  3317.    }
  3318.    
  3319.    String response;
  3320.    serializeJson(doc, response);
  3321.    request->send(200, "application/json", response);
  3322.  });
  3323.  
  3324.  // Ignition map API
  3325.  server.on("/ignitionMap", HTTP_GET, [](AsyncWebServerRequest *request) {
  3326.    StaticJsonDocument<16384> doc;
  3327.    
  3328.    doc["name"] = ignitionMap.name;
  3329.    doc["description"] = ignitionMap.description;
  3330.    doc["rpmLimit"] = tuning.rpmLimit;
  3331.    doc["rpmStep"] = tuning.rpmStep;
  3332.    doc["loadStep"] = tuning.loadStep;
  3333.    
  3334.    JsonArray rpmPoints = doc.createNestedArray("rpmPoints");
  3335.    for (int i = 0; i < ignitionMap.rpmPointCount; i++) {
  3336.      rpmPoints.add(ignitionMap.rpmPoints[i]);
  3337.    }
  3338.    
  3339.    JsonArray loadPoints = doc.createNestedArray("loadPoints");
  3340.    for (int i = 0; i < ignitionMap.loadPointCount; i++) {
  3341.      loadPoints.add(ignitionMap.loadPoints[i]);
  3342.    }
  3343.    
  3344.    JsonObject cells = doc.createNestedObject("cells");
  3345.    for (int i = 0; i < ignitionMap.rpmPointCount; i++) {
  3346.      JsonObject rpmCell = cells.createNestedObject(String(ignitionMap.rpmPoints[i]));
  3347.      for (int j = 0; j < ignitionMap.loadPointCount; j++) {
  3348.        rpmCell[String(ignitionMap.loadPoints[j])] = ignitionMap.cells[i][j].advance;
  3349.      }
  3350.    }
  3351.    
  3352.    String response;
  3353.    serializeJson(doc, response);
  3354.    request->send(200, "application/json", response);
  3355.  });
  3356.  
  3357.  server.on("/fuelMap/update", HTTP_POST, [](AsyncWebServerRequest *request) {
  3358.    if (request->contentType() == "application/json") {
  3359.      String body = request->arg("plain");
  3360.      StaticJsonDocument<8192> doc;
  3361.      deserializeJson(doc, body);
  3362.      
  3363.      JsonArray updates = doc["updates"];
  3364.      
  3365.      for(JsonObject update : updates) {
  3366.        int rpm = update["rpm"];
  3367.        int load = update["load"];
  3368.        int pulseWidth = update["pulseWidth"];
  3369.        
  3370.        int rpmIndex = findRPMIndex(rpm, true);
  3371.        int loadIndex = findLoadIndex(load, true);
  3372.        
  3373.        if (rpmIndex >= 0 && rpmIndex < fuelMap.rpmPointCount &&
  3374.            loadIndex >= 0 && loadIndex < fuelMap.loadPointCount) {
  3375.          fuelMap.cells[rpmIndex][loadIndex].pulseWidth = pulseWidth;
  3376.        }
  3377.      }
  3378.      
  3379.      saveTuningToEEPROM();
  3380.      request->send(200, "text/plain", "OK");
  3381.    } else {
  3382.      request->send(400, "text/plain", "Invalid content type");
  3383.    }
  3384.  });
  3385.  
  3386.  server.on("/ignitionMap/update", HTTP_POST, [](AsyncWebServerRequest *request) {
  3387.    if (request->contentType() == "application/json") {
  3388.      String body = request->arg("plain");
  3389.      StaticJsonDocument<8192> doc;
  3390.      deserializeJson(doc, body);
  3391.      
  3392.      JsonArray updates = doc["updates"];
  3393.      
  3394.      for(JsonObject update : updates) {
  3395.        int rpm = update["rpm"];
  3396.        int load = update["load"];
  3397.        float advance = update["advance"];
  3398.        
  3399.        int rpmIndex = findRPMIndex(rpm, false);
  3400.        int loadIndex = findLoadIndex(load, false);
  3401.        
  3402.        if (rpmIndex >= 0 && rpmIndex < ignitionMap.rpmPointCount &&
  3403.            loadIndex >= 0 && loadIndex < ignitionMap.loadPointCount) {
  3404.          ignitionMap.cells[rpmIndex][loadIndex].advance = advance;
  3405.        }
  3406.      }
  3407.      
  3408.      saveTuningToEEPROM();
  3409.      request->send(200, "text/plain", "OK");
  3410.    } else {
  3411.      request->send(400, "text/plain", "Invalid content type");
  3412.    }
  3413.  });
  3414.  
  3415.  // Fuel map management
  3416.  server.on("/autotune/fuel", HTTP_GET, [](AsyncWebServerRequest *request) {
  3417.    float afrError = o2Value - tuning.targetAFR;
  3418.    
  3419.    if (abs(afrError) > 0.5) {
  3420.      float correction = 1.0 + (afrError * 0.1);
  3421.      
  3422.      for (int i = 0; i < fuelMap.rpmPointCount; i++) {
  3423.        for (int j = 0; j < fuelMap.loadPointCount; j++) {
  3424.          fuelMap.cells[i][j].pulseWidth *= correction;
  3425.          fuelMap.cells[i][j].learned = true;
  3426.          fuelMap.cells[i][j].correction = correction;
  3427.        }
  3428.      }
  3429.      
  3430.      saveTuningToEEPROM();
  3431.    }
  3432.    
  3433.    request->send(200, "text/plain", "Fuel auto-tune completed");
  3434.  });
  3435.  
  3436.  // Ignition map management
  3437.  server.on("/autotune/ignition", HTTP_GET, [](AsyncWebServerRequest *request) {
  3438.    float knockCorrection = 1.0;
  3439.    
  3440.    // Simulate knock detection at high load/RPM
  3441.    if (currentRPM > 6000 && mapValue > 80) {
  3442.      knockCorrection = 0.95;
  3443.    }
  3444.    
  3445.    for (int i = 0; i < ignitionMap.rpmPointCount; i++) {
  3446.      for (int j = 0; j < ignitionMap.loadPointCount; j++) {
  3447.        ignitionMap.cells[i][j].advance *= knockCorrection;
  3448.        ignitionMap.cells[i][j].learned = true;
  3449.        ignitionMap.cells[i][j].correction = knockCorrection;
  3450.      }
  3451.    }
  3452.    
  3453.    saveTuningToEEPROM();
  3454.    request->send(200, "text/plain", "Ignition auto-tune completed");
  3455.  });
  3456.  
  3457.  server.on("/smoothMap/fuel", HTTP_GET, [](AsyncWebServerRequest *request) {
  3458.    for (int i = 1; i < fuelMap.rpmPointCount - 1; i++) {
  3459.      for (int j = 1; j < fuelMap.loadPointCount - 1; j++) {
  3460.        int avg = (fuelMap.cells[i-1][j].pulseWidth +
  3461.                   fuelMap.cells[i][j-1].pulseWidth +
  3462.                   fuelMap.cells[i][j].pulseWidth +
  3463.                   fuelMap.cells[i][j+1].pulseWidth +
  3464.                   fuelMap.cells[i+1][j].pulseWidth) / 5;
  3465.        fuelMap.cells[i][j].pulseWidth = avg;
  3466.      }
  3467.    }
  3468.    
  3469.    saveTuningToEEPROM();
  3470.    request->send(200, "text/plain", "Fuel map smoothed");
  3471.  });
  3472.  
  3473.  server.on("/smoothMap/ignition", HTTP_GET, [](AsyncWebServerRequest *request) {
  3474.    for (int i = 1; i < ignitionMap.rpmPointCount - 1; i++) {
  3475.      for (int j = 1; j < ignitionMap.loadPointCount - 1; j++) {
  3476.        float avg = (ignitionMap.cells[i-1][j].advance +
  3477.                     ignitionMap.cells[i][j-1].advance +
  3478.                     ignitionMap.cells[i][j].advance +
  3479.                     ignitionMap.cells[i][j+1].advance +
  3480.                     ignitionMap.cells[i+1][j].advance) / 5.0;
  3481.        ignitionMap.cells[i][j].advance = avg;
  3482.      }
  3483.    }
  3484.    
  3485.    saveTuningToEEPROM();
  3486.    request->send(200, "text/plain", "Ignition map smoothed");
  3487.  });
  3488.  
  3489.  server.on("/resetMap/fuel", HTTP_GET, [](AsyncWebServerRequest *request) {
  3490.    initializeFuelMap();
  3491.    saveTuningToEEPROM();
  3492.    request->send(200, "text/plain", "Fuel map reset");
  3493.  });
  3494.  
  3495.  server.on("/resetMap/ignition", HTTP_GET, [](AsyncWebServerRequest *request) {
  3496.    initializeIgnitionMap();
  3497.    saveTuningToEEPROM();
  3498.    request->send(200, "text/plain", "Ignition map reset");
  3499.  });
  3500.  
  3501.  server.on("/flashMap/fuel", HTTP_GET, [](AsyncWebServerRequest *request) {
  3502.    isFlashing = true;
  3503.    flashStartTime = millis();
  3504.    saveTuningToEEPROM();
  3505.    request->send(200, "text/plain", "Fuel map flashed to ECU");
  3506.  });
  3507.  
  3508.  server.on("/flashMap/ignition", HTTP_GET, [](AsyncWebServerRequest *request) {
  3509.    isFlashing = true;
  3510.    flashStartTime = millis();
  3511.    saveTuningToEEPROM();
  3512.    request->send(200, "text/plain", "Ignition map flashed to ECU");
  3513.  });
  3514.  
  3515.  server.on("/addRPM/fuel", HTTP_POST, [](AsyncWebServerRequest *request) {
  3516.    if (request->contentType() == "application/json") {
  3517.      String body = request->arg("plain");
  3518.      StaticJsonDocument<256> doc;
  3519.      deserializeJson(doc, body);
  3520.      
  3521.      int newRPM = doc["rpm"];
  3522.      
  3523.      if (fuelMap.rpmPointCount < MAX_RPM_POINTS) {
  3524.        fuelMap.rpmPoints[fuelMap.rpmPointCount] = newRPM;
  3525.        fuelMap.rpmPointCount++;
  3526.        
  3527.        // Simple bubble sort
  3528.        for (int i = 0; i < fuelMap.rpmPointCount - 1; i++) {
  3529.          for (int j = 0; j < fuelMap.rpmPointCount - i - 1; j++) {
  3530.            if (fuelMap.rpmPoints[j] > fuelMap.rpmPoints[j + 1]) {
  3531.              int temp = fuelMap.rpmPoints[j];
  3532.              fuelMap.rpmPoints[j] = fuelMap.rpmPoints[j + 1];
  3533.              fuelMap.rpmPoints[j + 1] = temp;
  3534.            }
  3535.          }
  3536.        }
  3537.        
  3538.        int newIndex = findRPMIndex(newRPM, true);
  3539.        for (int j = 0; j < fuelMap.loadPointCount; j++) {
  3540.          if (newIndex > 0 && newIndex < fuelMap.rpmPointCount - 1) {
  3541.            int prevRPM = fuelMap.rpmPoints[newIndex - 1];
  3542.            int nextRPM = fuelMap.rpmPoints[newIndex + 1];
  3543.            int prevPulse = fuelMap.cells[newIndex - 1][j].pulseWidth;
  3544.            int nextPulse = fuelMap.cells[newIndex + 1][j].pulseWidth;
  3545.            
  3546.            float ratio = (float)(newRPM - prevRPM) / (nextRPM - prevRPM);
  3547.            fuelMap.cells[newIndex][j].pulseWidth = prevPulse + (nextPulse - prevPulse) * ratio;
  3548.          } else {
  3549.            fuelMap.cells[newIndex][j].pulseWidth = 800 + (newRPM * 0.1) + (fuelMap.loadPoints[j] * 8);
  3550.          }
  3551.        }
  3552.        
  3553.        saveTuningToEEPROM();
  3554.        request->send(200, "text/plain", "Fuel RPM point added");
  3555.      } else {
  3556.        request->send(507, "text/plain", "Maximum fuel RPM points reached");
  3557.      }
  3558.    } else {
  3559.      request->send(400, "text/plain", "Invalid content type");
  3560.    }
  3561.  });
  3562.  
  3563.  server.on("/addRPM/ignition", HTTP_POST, [](AsyncWebServerRequest *request) {
  3564.    if (request->contentType() == "application/json") {
  3565.      String body = request->arg("plain");
  3566.      StaticJsonDocument<256> doc;
  3567.      deserializeJson(doc, body);
  3568.      
  3569.      int newRPM = doc["rpm"];
  3570.      
  3571.      if (ignitionMap.rpmPointCount < MAX_RPM_POINTS) {
  3572.        ignitionMap.rpmPoints[ignitionMap.rpmPointCount] = newRPM;
  3573.        ignitionMap.rpmPointCount++;
  3574.        
  3575.        // Simple bubble sort
  3576.        for (int i = 0; i < ignitionMap.rpmPointCount - 1; i++) {
  3577.          for (int j = 0; j < ignitionMap.rpmPointCount - i - 1; j++) {
  3578.            if (ignitionMap.rpmPoints[j] > ignitionMap.rpmPoints[j + 1]) {
  3579.              int temp = ignitionMap.rpmPoints[j];
  3580.              ignitionMap.rpmPoints[j] = ignitionMap.rpmPoints[j + 1];
  3581.              ignitionMap.rpmPoints[j + 1] = temp;
  3582.            }
  3583.          }
  3584.        }
  3585.        
  3586.        int newIndex = findRPMIndex(newRPM, false);
  3587.        for (int j = 0; j < ignitionMap.loadPointCount; j++) {
  3588.          if (newIndex > 0 && newIndex < ignitionMap.rpmPointCount - 1) {
  3589.            int prevRPM = ignitionMap.rpmPoints[newIndex - 1];
  3590.            int nextRPM = ignitionMap.rpmPoints[newIndex + 1];
  3591.            float prevAdvance = ignitionMap.cells[newIndex - 1][j].advance;
  3592.            float nextAdvance = ignitionMap.cells[newIndex + 1][j].advance;
  3593.            
  3594.            float ratio = (float)(newRPM - prevRPM) / (nextRPM - prevRPM);
  3595.            ignitionMap.cells[newIndex][j].advance = prevAdvance + (nextAdvance - prevAdvance) * ratio;
  3596.          } else {
  3597.            ignitionMap.cells[newIndex][j].advance = 10.0 + (newRPM * 0.001) + (ignitionMap.loadPoints[j] * 0.1);
  3598.          }
  3599.        }
  3600.        
  3601.        saveTuningToEEPROM();
  3602.        request->send(200, "text/plain", "Ignition RPM point added");
  3603.      } else {
  3604.        request->send(507, "text/plain", "Maximum ignition RPM points reached");
  3605.      }
  3606.    } else {
  3607.      request->send(400, "text/plain", "Invalid content type");
  3608.    }
  3609.  });
  3610.  
  3611.  server.on("/removeRPM/fuel", HTTP_POST, [](AsyncWebServerRequest *request) {
  3612.    if (request->contentType() == "application/json") {
  3613.      String body = request->arg("plain");
  3614.      StaticJsonDocument<256> doc;
  3615.      deserializeJson(doc, body);
  3616.      
  3617.      int removeRPM = doc["rpm"];
  3618.      int removeIndex = -1;
  3619.      
  3620.      for (int i = 0; i < fuelMap.rpmPointCount; i++) {
  3621.        if (fuelMap.rpmPoints[i] == removeRPM) {
  3622.          removeIndex = i;
  3623.          break;
  3624.        }
  3625.      }
  3626.      
  3627.      if (removeIndex != -1 && fuelMap.rpmPointCount > 1) {
  3628.        for (int i = removeIndex; i < fuelMap.rpmPointCount - 1; i++) {
  3629.          fuelMap.rpmPoints[i] = fuelMap.rpmPoints[i + 1];
  3630.          for (int j = 0; j < fuelMap.loadPointCount; j++) {
  3631.            fuelMap.cells[i][j] = fuelMap.cells[i + 1][j];
  3632.          }
  3633.        }
  3634.        fuelMap.rpmPointCount--;
  3635.        
  3636.        saveTuningToEEPROM();
  3637.        request->send(200, "text/plain", "Fuel RPM point removed");
  3638.      } else {
  3639.        request->send(400, "text/plain", "Fuel RPM point not found or cannot remove last point");
  3640.      }
  3641.    } else {
  3642.      request->send(400, "text/plain", "Invalid content type");
  3643.    }
  3644.  });
  3645.  
  3646.  server.on("/removeRPM/ignition", HTTP_POST, [](AsyncWebServerRequest *request) {
  3647.    if (request->contentType() == "application/json") {
  3648.      String body = request->arg("plain");
  3649.      StaticJsonDocument<256> doc;
  3650.      deserializeJson(doc, body);
  3651.      
  3652.      int removeRPM = doc["rpm"];
  3653.      int removeIndex = -1;
  3654.      
  3655.      for (int i = 0; i < ignitionMap.rpmPointCount; i++) {
  3656.        if (ignitionMap.rpmPoints[i] == removeRPM) {
  3657.          removeIndex = i;
  3658.          break;
  3659.        }
  3660.      }
  3661.      
  3662.      if (removeIndex != -1 && ignitionMap.rpmPointCount > 1) {
  3663.        for (int i = removeIndex; i < ignitionMap.rpmPointCount - 1; i++) {
  3664.          ignitionMap.rpmPoints[i] = ignitionMap.rpmPoints[i + 1];
  3665.          for (int j = 0; j < ignitionMap.loadPointCount; j++) {
  3666.            ignitionMap.cells[i][j] = ignitionMap.cells[i + 1][j];
  3667.          }
  3668.        }
  3669.        ignitionMap.rpmPointCount--;
  3670.        
  3671.        saveTuningToEEPROM();
  3672.        request->send(200, "text/plain", "Ignition RPM point removed");
  3673.      } else {
  3674.        request->send(400, "text/plain", "Ignition RPM point not found or cannot remove last point");
  3675.      }
  3676.    } else {
  3677.      request->send(400, "text/plain", "Invalid content type");
  3678.    }
  3679.  });
  3680.  
  3681.  server.begin();
  3682. }
  3683.  
  3684. // ==================== SETUP ====================
  3685. void setup() {
  3686.  Serial.begin(115200);
  3687.  
  3688.  // Initialize output pins
  3689.  pinMode(PIN_INJECTOR_1, OUTPUT);
  3690.  pinMode(PIN_INJECTOR_2, OUTPUT);
  3691.  pinMode(PIN_INJECTOR_3, OUTPUT);
  3692.  pinMode(PIN_INJECTOR_4, OUTPUT);
  3693.  digitalWrite(PIN_INJECTOR_1, LOW);
  3694.  digitalWrite(PIN_INJECTOR_2, LOW);
  3695.  digitalWrite(PIN_INJECTOR_3, LOW);
  3696.  digitalWrite(PIN_INJECTOR_4, LOW);
  3697.  
  3698.  pinMode(PIN_IGNITION_1, OUTPUT);
  3699.  pinMode(PIN_IGNITION_2, OUTPUT);
  3700.  pinMode(PIN_IGNITION_3, OUTPUT);
  3701.  pinMode(PIN_IGNITION_4, OUTPUT);
  3702.  digitalWrite(PIN_IGNITION_1, LOW);
  3703.  digitalWrite(PIN_IGNITION_2, LOW);
  3704.  digitalWrite(PIN_IGNITION_3, LOW);
  3705.  digitalWrite(PIN_IGNITION_4, LOW);
  3706.  
  3707.  pinMode(PIN_HEADLIGHT, OUTPUT);
  3708.  pinMode(PIN_HIGHBEAM, OUTPUT);
  3709.  pinMode(PIN_LEFT_TURN, OUTPUT);
  3710.  pinMode(PIN_RIGHT_TURN, OUTPUT);
  3711.  pinMode(PIN_BRAKE_LIGHT, OUTPUT);
  3712.  digitalWrite(PIN_HEADLIGHT, LOW);
  3713.  digitalWrite(PIN_HIGHBEAM, LOW);
  3714.  digitalWrite(PIN_LEFT_TURN, LOW);
  3715.  digitalWrite(PIN_RIGHT_TURN, LOW);
  3716.  digitalWrite(PIN_BRAKE_LIGHT, LOW);
  3717.  
  3718.  pinMode(PIN_O2_SENSOR, INPUT);
  3719.  pinMode(PIN_TPS, INPUT);
  3720.  pinMode(PIN_MAP_SENSOR, INPUT);
  3721.  pinMode(PIN_ENGINE_TEMP, INPUT);
  3722.  
  3723.  pinMode(PIN_RPM, INPUT_PULLUP);
  3724.  attachInterrupt(digitalPinToInterrupt(PIN_RPM), rpmInterrupt, RISING);
  3725.  
  3726.  initializeFuelMap();
  3727.  initializeIgnitionMap();
  3728.  loadTuningFromEEPROM();
  3729.  
  3730.  WiFi.softAP(ssid, password);
  3731.  
  3732.  setupWebServer();
  3733.  
  3734.  Serial.println("Professional 4-Cylinder ECU Started");
  3735.  Serial.print("IP Address: ");
  3736.  Serial.println(WiFi.softAPIP());
  3737.  Serial.println("Connect to WiFi: ESP32_ECU");
  3738.  Serial.println("Password: tune12345");
  3739.  Serial.println("Open browser to: http://192.168.4.1");
  3740. }
  3741.  
  3742. // ==================== MAIN LOOP ====================
  3743. void loop() {
  3744.   readSensors();
  3745.   updateLighting();
  3746.  
  3747.   if (isFlashing && (millis() - flashStartTime > 2000)) {
  3748.     isFlashing = false;
  3749.   }
  3750.  
  3751.   if (!performSafetyChecks()) {
  3752.     if (!criticalFault) {
  3753.       activateFailSafe();
  3754.     }
  3755.   } else {
  3756.     criticalFault = false;
  3757.   }
  3758.  
  3759.   if (!criticalFault && systemEnabled) {
  3760.     controlInjection();
  3761.     controlIgnition();
  3762.   } else {
  3763.     for (int i = 0; i < 4; i++) {
  3764.       digitalWrite(PIN_INJECTOR_1 + i, LOW);
  3765.       digitalWrite(PIN_IGNITION_1 + i, LOW);
  3766.     }
  3767.   }
  3768.  
  3769.   delay(10);
  3770. }
Advertisement
Add Comment
Please, Sign In to add comment