#include <math.h>
#include <WiFi.h>
#include <WebServer.h>
/* ============================= */
/* CONFIGURATION AP & WEB SERVER */
/* ============================= */
const char* ssid = "FEROZA_ECU_V3";
const char* password = "password123"; // Minimal 8 karakter
WebServer server(80);
/* ============================= */
/* PIN CONFIG */
/* ============================= */
#define RPM_DIGITAL_PIN 33 // Input digital dari pulsa CDI
#define MAP_PIN 34
#define TRIM_PIN 35
#define TEMP_PIN 32
#define INJECTOR_PIN 25
/* ============================= */
/* VARIABLES */
/* ============================= */
float rpm = 0;
float rpmFiltered = 0;
volatile unsigned long lastPulseTime = 0;
volatile unsigned long pulseInterval = 0;
volatile bool newPulseDetected = false;
volatile bool triggerInjectorNow = false;
float mapVoltage = 0;
float mapFiltered = 0;
float fuelTrim = 1.0; // Nilai ini sekarang bisa di-overide dari Web Menu
float coolantTemp = 25.0;
float warmupFactor = 1.0;
float accelFuel = 1.0;
float lastMapVoltage = 0;
float afrTarget = 14.7;
float currentPW = 0;
bool injectorState = false;
unsigned long injectorStart = 0;
unsigned long accelTimer = 0;
unsigned long lastTrimRead = 0;
/* ============================= */
/* INTERRUPT SERVICE ROUTINE */
/* ============================= */
void IRAM_ATTR rpmPulseISR() {
unsigned long now = micros();
pulseInterval = now - lastPulseTime;
lastPulseTime = now;
newPulseDetected = true;
triggerInjectorNow = true;
}
/* ============================= */
/* WEB SERVER HANDLERS */
/* ============================= */
// Halaman Utama Menu Tuning (HTML + CSS + Javascript Auto-Refresh)
void handleRoot() {
String html = "<!DOCTYPE html><html><head><meta name='viewport' content='width=device-width, initial-scale=1.0'>";
html += "<title>FEROZA ECU TUNING</title>";
html += "<style>";
html += "body { font-family: Arial, sans-serif; background: #1a1a1a; color: #fff; text-align: center; padding: 20px; }";
html += ".container { max-width: 500px; margin: 0 auto; background: #2d2d2d; padding: 20px; border-radius: 10px; box-shadow: 0 4px 8px rgba(0,0,0,0.5); }";
html += "h1 { color: #ff3333; margin-bottom: 20px; }";
html += ".grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 20px; }";
html += ".card { background: #3d3d3d; padding: 15px; border-radius: 5px; font-size: 1.2em; }";
html += ".card span { display: block; font-size: 0.6em; color: #aaa; text-transform: uppercase; }";
html += "input[type=number] { width: 80px; padding: 8px; font-size: 1em; text-align: center; border-radius: 5px; border: none; }";
html += "input[type=submit] { background: #00cc66; color: white; border: none; padding: 10px 20px; font-size: 1em; border-radius: 5px; cursor: pointer; width: 100%; margin-top: 10px; }";
html += "input[type=submit]:hover { background: #009951; }";
html += "</style>";
// Script untuk mengambil data sensor secara realtime tanpa reload halaman (AJAX)
html += "<script>";
html += "setInterval(function() {";
html += " fetch('/data').then(response => response.json()).then(data => {";
html += " document.getElementById('val-rpm').innerText = data.rpm;";
html += " document.getElementById('val-map').innerText = data.map;";
html += " document.getElementById('val-temp').innerText = data.temp;";
html += " document.getElementById('val-pw').innerText = data.pw;";
html += " document.getElementById('val-load').innerText = data.load;";
html += " });";
html += "}, 300);"; // Update setiap 300ms
html += "</script>";
html += "</head><body>";
html += "<div class='container'>";
html += "<h1>FEROZA ECU V3</h1>";
// Dashboard Live Data
html += "<div class='grid'>";
html += "<div class='card'><div id='val-rpm'>0</div><span>RPM</span></div>";
html += "<div class='card'><div id='val-map'>0.00</div><span>MAP (Volt)</span></div>";
html += "<div class='card'><div id='val-temp'>0.0</div><span>Coolant Temp (°C)</span></div>";
html += "<div class='card'><div id='val-load'>0.00</div><span>Engine Load</span></div>";
html += "</div>";
html += "<div class='card' style='grid-column: span 2; background:#4d2222; margin-bottom:20px;'><div id='val-pw' style='font-size:1.5em; font-weight:bold; color:#ffcc00;'>0.00</div><span>Pulse Width (ms)</span></div>";
// Form Tuning Manual Fuel Trim
html += "<form action='/save' method='POST'>";
html += "<h3>Manual Tuning</h3>";
html += "<label style='font-size: 1.1em;'>Fuel Trim Multiplier: </label>";
html += "<input type='number' name='trim' step='0.01' min='0.5' max='1.5' value='" + String(fuelTrim, 2) + "'>";
html += "<br><span style='font-size:0.8em; color:#aaa;'>(Default: 1.00 | Range: 0.50 - 1.50)</span>";
html += "<input type='submit' value='Simpan Konfigurasi'>";
html += "</form>";
html += "</div></body></html>";
server.send(200, "text/html", html);
}
// Endpoint JSON untuk mensuplai data realtime ke Javascript halaman utama
void handleData() {
String json = "{";
json += "\"rpm\":" + String(rpm, 0) + ",";
json += "\"map\":" + String(mapVoltage, 2) + ",";
json += "\"temp\":" + String(coolantTemp, 1) + ",";
json += "\"load\":" + String(getEngineLoad(), 2) + ",";
json += "\"pw\":" + String(currentPW, 2);
json += "}";
server.send(200, "application/json", json);
}
// Endpoint untuk memproses penyimpanan data form tuning
void handleSave() {
if (server.hasArg("trim")) {
fuelTrim = server.arg("trim").toFloat();
Serial.print("Fuel Trim berhasil diubah via Web: ");
Serial.println(fuelTrim);
}
// Alihkan kembali user ke halaman utama setelah klik simpan
server.sendHeader("Location", "/");
server.send(333);
}
/* ============================= */
/* SETUP */
/* ============================= */
void setup()
{
Serial.begin(115200);
delay(1000);
Serial.println("\n=================================");
Serial.println("FEROZA ECU V3 - WIFI TUNING ACTIVE");
Serial.println("=================================");
pinMode(INJECTOR_PIN, OUTPUT);
digitalWrite(INJECTOR_PIN, LOW);
pinMode(RPM_DIGITAL_PIN, INPUT);
attachInterrupt(digitalPinToInterrupt(RPM_DIGITAL_PIN), rpmPulseISR, RISING);
analogReadResolution(12);
// Inisialisasi Access Point ESP32
WiFi.softAP(ssid, password);
IPAddress IP = WiFi.softAPIP();
Serial.print("Access Point Aktif. SSID: ");
Serial.println(ssid);
Serial.print("Buka browser dan ketik IP: ");
Serial.println(IP); // Biasanya secara default adalah 192.168.4.1
// Routing Web Server
server.on("/", handleRoot);
server.on("/data", handleData);
server.on("/save", handleSave);
server.begin();
Serial.println("Web Server Berhasil Dijalankan!");
}
/* ============================= */
/* MAIN LOOP */
/* ============================= */
void loop()
{
// Handle request Web Server secara periodik
server.handleClient();
readRPM();
readMAP();
readTemperature();
calculateWarmup();
accelerationEnrichment();
/* Keterangan: Fungsi readFuelTrim() via Potensiometer fisik (Pin 35) dinonaktifkan
agar tidak menimpa (*overwrite*) nilai Fuel Trim yang kamu input dari HP/Web. */
if (rpm > 50)
currentPW = calculatePulseWidth();
else
currentPW = 0;
injectorTask();
serialMonitor();
}
/* ============================= */
/* RPM (DIGITAL PULSE) */
/* ============================= */
void readRPM()
{
float newRPM = 0;
unsigned long currentMicros = micros();
noInterrupts();
unsigned long interval = pulseInterval;
bool pulseSeen = newPulseDetected;
newPulseDetected = false;
interrupts();
if (currentMicros - lastPulseTime > 1000000) {
newRPM = 0;
}
else if (pulseSeen && interval > 0) {
newRPM = 30000000.0 / interval;
}
else {
return;
}
if (newRPM > 8000) {
newRPM = rpm;
}
rpmFiltered = (rpmFiltered * 0.80) + (newRPM * 0.20);
rpm = rpmFiltered;
}
/* ============================= */
/* INJECTOR TASK (SYNCHRONOUS) */
/* ============================= */
void injectorTask()
{
if (rpm < 50)
{
digitalWrite(INJECTOR_PIN, LOW);
injectorState = false;
triggerInjectorNow = false;
return;
}
unsigned long now = micros();
if (triggerInjectorNow && !injectorState)
{
digitalWrite(INJECTOR_PIN, HIGH);
injectorStart = now;
injectorState = true;
triggerInjectorNow = false;
}
if (injectorState)
{
if ((now - injectorStart) >= (currentPW * 1000))
{
digitalWrite(INJECTOR_PIN, LOW);
injectorState = false;
}
}
}
/* ============================= */
/* MAP */
/* ============================= */
void readMAP()
{
int adc = analogRead(MAP_PIN);
float rawVoltage = adc * (3.3 / 4095.0);
mapFiltered = (mapFiltered * 0.8) + (rawVoltage * 0.2);
mapVoltage = mapFiltered;
}
/* ============================= */
/* TEMPERATURE */
/* ============================= */
void readTemperature()
{
int adc = analogRead(TEMP_PIN);
float voltage = adc * (3.3 / 4095.0);
if (voltage < 0.05) voltage = 0.05;
float resistance = (10000.0 * voltage) / (3.3 - voltage);
float tempK = 1.0 / ((1.0 / 298.15) + (1.0 / 3950.0) * log(resistance / 10000.0));
coolantTemp = tempK - 273.15;
}
/* ============================= */
/* WARMUP */
/* ============================= */
void calculateWarmup()
{
if (coolantTemp < 10) warmupFactor = 1.60;
else if (coolantTemp < 20) warmupFactor = 1.45;
else if (coolantTemp < 30) warmupFactor = 1.30;
else if (coolantTemp < 40) warmupFactor = 1.20;
else if (coolantTemp < 60) warmupFactor = 1.10;
else warmupFactor = 1.00;
}
/* ============================= */
/* ACCEL ENRICHMENT */
/* ============================= */
void accelerationEnrichment()
{
float deltaMap = mapVoltage - lastMapVoltage;
lastMapVoltage = mapVoltage;
if (deltaMap > 0.30) accelFuel = 1.35;
else if (deltaMap > 0.15) accelFuel = 1.20;
else if (deltaMap > 0.05) accelFuel = 1.10;
if (millis() - accelTimer > 250)
accelFuel = 1.00;
else
accelTimer = millis();
}
/* ============================= */
/* ENGINE LOAD */
/* ============================= */
float getEngineLoad()
{
float mapNorm = mapVoltage / 3.3;
float rpmNorm = rpm / 6000.0;
float load = mapNorm * (0.6 + rpmNorm);
if (load < 0.2) load = 0.2;
if (load > 1.5) load = 1.5;
return load;
}
/* ============================= */
/* AFR TARGET */
/* ============================= */
float getAfrTarget()
{
if (rpm < 1000) return 13.8;
else if (rpm < 2500) return 14.7;
else if (rpm < 4000) return 13.5;
else return 12.8;
}
/* ============================= */
/* PULSE WIDTH */
/* ============================= */
float calculatePulseWidth()
{
float basePW;
if (rpm < 600) basePW = 4.5;
else if (rpm < 1000) basePW = 1.8;
else if (rpm < 2000) basePW = 2.5;
else if (rpm < 3000) basePW = 3.5;
else if (rpm < 4000) basePW = 5.0;
else basePW = 6.5;
float load = getEngineLoad();
afrTarget = getAfrTarget();
float fuel = basePW * load;
float afrCorrection = 14.7 / afrTarget;
float pw = fuel * afrCorrection;
pw *= fuelTrim; // Menggunakan nilai trim yang diinput dari web
pw *= accelFuel;
pw *= warmupFactor;
if (mapVoltage > 2.80) pw *= 1.15;
if (rpm < 1500 && mapVoltage > 2.5) pw *= 1.25;
return pw;
}
/* ============================= */
/* SERIAL MONITOR */
/* ============================= */
void serialMonitor()
{
static unsigned long lastPrint = 0;
if (millis() - lastPrint > 500)
{
Serial.print("RPM="); Serial.print(rpm, 0);
Serial.print(" MAP="); Serial.print(mapVoltage, 2);
Serial.print(" TEMP="); Serial.print(coolantTemp, 1);
Serial.print(" WARM="); Serial.print(warmupFactor, 2);
Serial.print(" LOAD="); Serial.print(getEngineLoad(), 2);
Serial.print(" AFR_T="); Serial.print(afrTarget, 1);
Serial.print(" TRIM="); Serial.print(fuelTrim, 2);
Serial.print(" ACC="); Serial.print(accelFuel, 2);
Serial.print(" PW="); Serial.println(currentPW, 2);
lastPrint = millis();
}
}