HumanixTechLab
Published © CC BY-NC-SA

Two-Faced: A 4WD WiFi RC Car Controlled by Your Browser

A dual-tone 4WD RC car built with ESP8266 + L298N, driven entirely from a color-coded web joystick — no app, no remote needed.

IntermediateFull instructions provided3 hours76
Two-Faced: A 4WD WiFi RC Car Controlled by Your Browser

Things used in this project

Hardware components

NodeMCU ESP8266 Breakout Board
NodeMCU ESP8266 Breakout Board
×1
Dual H-Bridge motor drivers L298
SparkFun Electronics Dual H-Bridge motor drivers L298
×1
DC Motor, 12 V
DC Motor, 12 V
×4
LED (generic)
LED (generic)
×4
Resistor 100 ohm
Resistor 100 ohm
×2
18650 2500mah
×2
pvc sheets
×1
12v dc connector
×1
Power Switch
×1
BC547 Transisitor
×1

Software apps and online services

Any Browser

Story

Read more

Schematics

Circuit

Circuit Diagram

Code

Arduino Code

Arduino
esp8266 Code
/**
 * ================================================================
 *        WiFi RC CAR CONTROLLER  ESP8266 NodeMCU
 * ================================================================
 *
 * Project : WiFi RC Car using ESP8266 NodeMCU
 * Brand   : Humanix Tech
 * Author  : Humanix Tech
 * Version : 1.0
 * Board   : ESP8266 NodeMCU / Wemos D1 Mini
 * GitHub  : https://github.com/humanixtech
 * Video   : <YouTube link (optional)>
 * HOW IT WORKS:
 *   ESP8266 creates its own WiFi hotspot  connect your phone
 *    open browser  http://192.168.4.1  control the car!
 *
 * WIRING SUMMARY:
 *   L298N IN1=D1, IN2=D2, IN3=D5, IN4=D6
 *   L298N ENA=D7, ENB=D0
 *   Red  LED  D3 (Active HIGH, 100 to GND)
 *   White LED  D8 (Active HIGH, 100 to GND)
 *
 * ----------------------------------------------------------------
 *    If this project helped you:
 *    LIKE the video
 *    SUBSCRIBE for more ESP8266 / Arduino projects
 *    STAR this repo on GitHub
 *    COMMENT below with questions or your build!
 *    SHARE with your friends who love robotics!
 *  Your support keeps this going  thank you! 
 * ================================================================
**/



#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>

const char* ssid = "CAR_v1.0";
const char* password = "12345678";

ESP8266WebServer server(80);

// ===== PINS =====
#define IN1 D1
#define IN2 D2
#define IN3 D5
#define IN4 D6
#define ENA D7
#define ENB D0
#define RED_LED   D3   // external -- ACTIVE HIGH (HIGH=ON, LOW=OFF)
#define WHITE_LED D8   // external -- ACTIVE HIGH (HIGH=ON, LOW=OFF)

int driveSpeed = 512;
int turnSpeed  = 512;

// ===== PARKING / HAZARD BLINK STATE =====
bool parkingOn       = false;       // true when PARKING button is ON
bool redLedState     = false;       // current physical LED state
unsigned long lastBlinkTime = 0;
const unsigned long BLINK_INTERVAL = 500; // ms  blink every 500ms (1 Hz)

// ===== WEBPAGE =====
String webpage = R"=====(
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>CAR CTRL</title>
<style>

*, *::before, *::after { box-sizing: border-box; margin:0; padding:0; }

html, body {
  width:100%; height:100%;
  overflow:hidden;
  font-family: 'Segoe UI', Tahoma, sans-serif;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  user-select: none;
  background: #0b0d12;
}

/*  MAIN LAYOUT  */
.container {
  display: flex;
  flex-direction: row;
  justify-content: center;
  align-items: center;
  height: 100dvh;
  gap: 18px;
  padding: 20px 16px;
}

/*  PANEL CARD  */
.panel {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  border-radius: 26px;
  padding: 20px 16px 18px;
  gap: 14px;
  position: relative;
}

/* 
   SECTION 1  DRIVE (Blue)
 */
.panel-move {
  background: linear-gradient(160deg, #0d1e3a, #071428);
  border: 1px solid rgba(33,150,243,0.25);
  box-shadow:
    0 8px 32px rgba(0,0,0,0.55),
    inset 0 1px 0 rgba(33,150,243,0.15),
    0 0 40px rgba(33,150,243,0.06);
  min-width: 120px;
}

.btn-fwd {
  width: 110px;
  height: 100px;
  clip-path: polygon(50% 0%, 100% 100%, 0% 100%);
  background: linear-gradient(175deg, #2979ff, #1565c0);
  box-shadow: none;
  border: none;
  border-radius: 0;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  padding-bottom: 10px;
  color: white;
  font-size: 13px;
  font-weight: 900;
  letter-spacing: 2px;
  cursor: pointer;
  transition: filter 0.1s ease, transform 0.08s ease;
  -webkit-tap-highlight-color: transparent;
  text-transform: uppercase;
  position: relative;
  filter: drop-shadow(0 6px 14px rgba(41,121,255,0.5));
}
.btn-fwd::after {
  content: 'FWD';
  position: absolute;
  bottom: 14px;
  font-size: 12px;
  font-weight: 900;
  letter-spacing: 1.5px;
  color: rgba(255,255,255,0.9);
}
.btn-fwd.pressing {
  filter: drop-shadow(0 2px 20px rgba(130,177,255,0.9)) brightness(1.3);
  transform: scale(0.95) translateY(4px);
}

.btn-rev {
  width: 110px;
  height: 100px;
  clip-path: polygon(0% 0%, 100% 0%, 50% 100%);
  background: linear-gradient(175deg, #5c9fff, #1976d2);
  box-shadow: none;
  border: none;
  border-radius: 0;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding-top: 10px;
  color: white;
  font-size: 13px;
  font-weight: 900;
  letter-spacing: 2px;
  cursor: pointer;
  transition: filter 0.1s ease, transform 0.08s ease;
  -webkit-tap-highlight-color: transparent;
  text-transform: uppercase;
  position: relative;
  filter: drop-shadow(0 6px 14px rgba(68,138,255,0.45));
}
.btn-rev::after {
  content: 'REV';
  position: absolute;
  top: 14px;
  font-size: 12px;
  font-weight: 900;
  letter-spacing: 1.5px;
  color: rgba(255,255,255,0.9);
}
.btn-rev.pressing {
  filter: drop-shadow(0 2px 20px rgba(144,202,249,0.9)) brightness(1.3);
  transform: scale(0.95) translateY(-4px);
}

/* 
   SECTION 2  CONTROL (Purple)
 */
.panel-center {
  background: linear-gradient(160deg, #1a1226, #0e0b18);
  border: 1px solid rgba(180,100,255,0.2);
  box-shadow:
    0 8px 32px rgba(0,0,0,0.6),
    inset 0 1px 0 rgba(180,100,255,0.1),
    0 0 40px rgba(140,80,255,0.05);
  padding: 18px 14px 16px;
  gap: 12px;
}

.grid4 {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 12px;
}

/*  EMERGENCY STOP  */
.btn-estop {
  width: 100%;
  height: 46px;
  background: linear-gradient(175deg, #4a0000, #2a0000);
  border: 1.5px solid rgba(255,50,50,0.3);
  border-radius: 12px;
  color: #ff6b6b;
  font-size: 12px;
  font-weight: 900;
  letter-spacing: 3px;
  cursor: pointer;
  text-transform: uppercase;
  transition: all 0.1s ease;
  -webkit-tap-highlight-color: transparent;
  box-shadow:
    0 5px 0 #1a0000,
    0 8px 20px rgba(180,0,0,0.25),
    inset 0 1px 0 rgba(255,80,80,0.1);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
}
.btn-estop.active {
  background: linear-gradient(175deg, #ff1a1a, #cc0000) !important;
  color: white !important;
  border-color: rgba(255,120,120,0.8) !important;
  transform: translateY(3px) !important;
  box-shadow:
    0 2px 0 #7a0000,
    0 0 40px rgba(255,0,0,1),
    0 0 80px rgba(255,0,0,0.5),
    inset 0 1px 0 rgba(255,200,200,0.3) !important;
  animation: estopPulse 0.6s ease-in-out infinite alternate;
}
@keyframes estopPulse {
  from { box-shadow: 0 2px 0 #7a0000, 0 0 30px rgba(255,0,0,0.8), 0 0 60px rgba(255,0,0,0.4); }
  to   { box-shadow: 0 2px 0 #7a0000, 0 0 55px rgba(255,0,0,1),   0 0 100px rgba(255,0,0,0.6); }
}
.estop-dot {
  width: 8px; height: 8px;
  border-radius: 50%;
  background: currentColor;
  opacity: 0.8;
}
.btn-estop.active .estop-dot {
  animation: dotBlink 0.4s ease-in-out infinite alternate;
}
@keyframes dotBlink {
  from { opacity: 0.4; } to { opacity: 1; }
}

/*  PARKING / HAZARD (RED) button  blinks UI when ON  */
.btn-red {

  width: 90px; height: 90px;
  border: none; border-radius: 18px;
  background: linear-gradient(175deg, #5c1010, #3a0000);
  box-shadow: 0 7px 0 #1a0000, 0 10px 18px rgba(180,0,0,0.3), inset 0 1px 0 rgba(255,100,100,0.15);
  color: #ff8a80;
  font-size: 11px; font-weight: 800; letter-spacing: 1px;
  cursor: pointer;
  transition: transform 0.08s ease, box-shadow 0.1s ease, background 0.1s ease;
  -webkit-tap-highlight-color: transparent;
  text-transform: uppercase;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px;
}
/* Blinking animation for PARKING ON state */
@keyframes hazardBlink {
  0%,100% {
    background: linear-gradient(175deg, #ff3333, #cc0000);
    color: white;
    box-shadow: 0 4px 0 #7a0000, 0 0 35px rgba(255,80,0,1), 0 0 70px rgba(255,80,0,0.5), inset 0 1px 0 rgba(255,200,200,0.3);
    transform: translateY(3px);
  }
  50% {
    background: linear-gradient(175deg, #5c1010, #3a0000);
    color: #ff8a80;
    box-shadow: 0 7px 0 #1a0000, 0 10px 18px rgba(180,0,0,0.3), inset 0 1px 0 rgba(255,100,100,0.15);
    transform: translateY(0px);
  }
}
.btn-red.on {
  animation: hazardBlink 0.5s ease-in-out infinite !important;
}

/* Hazard triangle icon () inside button */
.hazard-icon {
  font-size: 18px;
  line-height: 1;
}

/*  LIGHTS (WHITE) button  solid ON  */
.btn-white {
  width: 90px; height: 90px;
  border: none; border-radius: 18px;
  background: linear-gradient(175deg, #3a3a3a, #222);
  box-shadow: 0 7px 0 #111, 0 10px 18px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.08);
  color: #aaa;
  font-size: 11px; font-weight: 800; letter-spacing: 1px;
  cursor: pointer;
  transition: transform 0.08s ease, box-shadow 0.1s ease, background 0.1s ease;
  -webkit-tap-highlight-color: transparent;
  text-transform: uppercase;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px;
}
.btn-white.on {
  background: linear-gradient(175deg, #ffffff, #dddddd) !important;
  color: #111 !important;
  transform: translateY(3px);
  box-shadow: 0 4px 0 #999, 0 0 35px rgba(255,255,255,0.95), 0 0 70px rgba(255,255,255,0.4), inset 0 1px 0 rgba(255,255,255,0.5) !important;
}

/* Light beam icon */
.light-icon {
  font-size: 18px;
  line-height: 1;
}

/*  M1 SWITCH  */
.btn-m1 {
  width: 90px; height: 90px;
  border: none; border-radius: 18px;
  background: linear-gradient(175deg, #7a6010, #5a4500);
  color: #ffd54f;
  box-shadow: 0 7px 0 #3a2c00, 0 10px 18px rgba(100,80,0,0.3), inset 0 1px 0 rgba(255,220,100,0.12);
  font-size: 13px; font-weight: 800; letter-spacing: 1px;
  cursor: pointer;
  transition: transform 0.08s ease, box-shadow 0.1s ease, background 0.1s ease;
  -webkit-tap-highlight-color: transparent;
  text-transform: uppercase;
  display: flex; align-items: center; justify-content: center;
}
.btn-m1.on {
  background: linear-gradient(175deg, #ffca28, #f9a825) !important;
  color: #1a0f00 !important;
  transform: translateY(3px);
  box-shadow: 0 4px 0 #b37600, 0 0 25px rgba(255,200,0,0.8), 0 0 50px rgba(255,200,0,0.3), inset 0 1px 0 rgba(255,255,255,0.35) !important;
}

/*  M2 SWITCH  */
.btn-m2 {
  width: 90px; height: 90px;
  border: none; border-radius: 18px;
  background: linear-gradient(175deg, #0a4a52, #053035);
  color: #4dd0e1;
  box-shadow: 0 5px 0 #021e22, 0 8px 18px rgba(0,80,90,0.3), inset 0 1px 0 rgba(0,200,220,0.1);
  font-size: 13px; font-weight: 800; letter-spacing: 1px;
  cursor: pointer;
  transition: transform 0.08s ease, box-shadow 0.1s ease, background 0.1s ease;
  -webkit-tap-highlight-color: transparent;
  text-transform: uppercase;
  display: flex; align-items: center; justify-content: center;
}
.btn-m2.on {
  background: linear-gradient(175deg, #26c6da, #00838f) !important;
  color: white !important;
  transform: translateY(3px);
  box-shadow: 0 4px 0 #005662, 0 0 25px rgba(0,200,220,0.8), 0 0 50px rgba(0,200,220,0.3), inset 0 1px 0 rgba(255,255,255,0.25) !important;
}

/* 
   SECTION 3  STEER (Green)
 */
.panel-turn {
  background: linear-gradient(160deg, #0a2010, #061408);
  border: 1px solid rgba(76,175,80,0.25);
  box-shadow:
    0 8px 32px rgba(0,0,0,0.55),
    inset 0 1px 0 rgba(76,175,80,0.12),
    0 0 40px rgba(76,175,80,0.05);
  flex-direction: column;
  gap: 10px;
  padding: 20px 14px 18px;
}

.steer-row {
  display: flex;
  flex-direction: row;
  gap: 10px;
  align-items: center;
}

.btn-left {
  width: 100px; height: 80px;
  clip-path: polygon(0% 50%, 100% 0%, 100% 100%);
  background: linear-gradient(to right, #43a047, #2e7d32);
  border: none; border-radius: 0;
  display: flex; align-items: center; justify-content: center;
  padding-left: 28px;
  color: white; font-size: 12px; font-weight: 900; letter-spacing: 1.5px;
  cursor: pointer;
  transition: filter 0.1s ease, transform 0.08s ease;
  -webkit-tap-highlight-color: transparent;
  text-transform: uppercase;
  position: relative;
  filter: drop-shadow(0 4px 12px rgba(67,160,71,0.5));
}
.btn-left::after {
  content: 'LEFT';
  position: absolute; right: 12px;
  font-size: 12px; font-weight: 900;
  color: rgba(255,255,255,0.9); letter-spacing: 1px;
}
.btn-left.pressing {
  filter: drop-shadow(0 2px 20px rgba(165,214,167,0.9)) brightness(1.3);
  transform: scale(0.95) translateX(-4px);
}

.btn-right {
  width: 100px; height: 80px;
  clip-path: polygon(0% 0%, 100% 50%, 0% 100%);
  background: linear-gradient(to left, #66bb6a, #388e3c);
  border: none; border-radius: 0;
  display: flex; align-items: center; justify-content: center;
  padding-right: 28px;
  color: white; font-size: 12px; font-weight: 900; letter-spacing: 1.5px;
  cursor: pointer;
  transition: filter 0.1s ease, transform 0.08s ease;
  -webkit-tap-highlight-color: transparent;
  text-transform: uppercase;
  position: relative;
  filter: drop-shadow(0 4px 12px rgba(102,187,106,0.5));
}
.btn-right::after {
  content: 'RIGHT';
  position: absolute; left: 12px;
  font-size: 11px; font-weight: 900;
  color: rgba(255,255,255,0.9); letter-spacing: 1px;
}
.btn-right.pressing {
  filter: drop-shadow(0 2px 20px rgba(200,230,201,0.9)) brightness(1.3);
  transform: scale(0.95) translateX(4px);
}

/*  LANDSCAPE  */
@media (orientation: landscape) {
  .container {
    flex-direction: row;
    justify-content: space-evenly;
    padding: 10px 16px;
    gap: 16px;
  }
  .panel { padding: 14px 12px 12px; gap: 10px; border-radius: 20px; }
  .btn-fwd, .btn-rev { width: 96px; height: 88px; }
  .btn-red, .btn-white, .btn-m1, .btn-m2 { width: 78px; height: 78px; font-size: 11px; }
  .btn-estop { height: 40px; font-size: 11px; }
  .btn-left, .btn-right { width: 88px; height: 70px; }
  .grid4 { gap: 9px; }
}

body.estop-mode {
  animation: bodyRedFlash 0.5s ease-in-out infinite alternate;
}
@keyframes bodyRedFlash {
  from { background: #0b0d12; }
  to   { background: #1a0303; }
}

</style>
</head>
<body>

<div class="container">

  <!-- SECTION 1: DRIVE -->
  <div class="panel panel-move">
    <button id="forward"  class="btn-fwd"></button>
    <button id="backward" class="btn-rev"></button>
  </div>

  <!-- SECTION 2: CONTROL -->
  <div class="panel panel-center">
    <div class="grid4">
      <!-- PARKING: blinks RED LED when ON -->
      <button id="redLight" class="btn-red">
        <span class="hazard-icon"></span>
        Hazard
      </button>
      <!-- LIGHTS: solid WHITE LED when ON -->
      <button id="whiteLight" class="btn-white">
        <span class="light-icon"></span>
        LIGHTS
      </button>
      <button id="mode1" class="btn-m1">M1</button>
      <button id="mode2" class="btn-m2">M2</button>
    </div>
    <button id="eStop" class="btn-estop">
      <span class="estop-dot"></span>
      STOP
      <span class="estop-dot"></span>
    </button>
  </div>

  <!-- SECTION 3: STEER -->
  <div class="panel panel-turn">
    <div class="steer-row">
      <button id="left"  class="btn-left"></button>
      <button id="right" class="btn-right"></button>
    </div>
  </div>

</div>

<script>
var emergencyStop = false;

var toggleStates = {
  redLight:   false,
  whiteLight: false,
  mode1:      false,
  mode2:      false
};

function resetAllToggles(){
  var ids = ['redLight','whiteLight','mode1','mode2'];
  for(var k = 0; k < ids.length; k++){
    var id = ids[k];
    if(toggleStates[id]){
      toggleStates[id] = false;
      document.getElementById(id).classList.remove('on');
      fetch('/' + id + '?state=false');
    }
  }
  fetch('/stop');
}

//  EMERGENCY STOP 
var eStopBtn = document.getElementById('eStop');
function activateEStop(){
  emergencyStop = !emergencyStop;
  if(emergencyStop){
    eStopBtn.classList.add('active');
    document.body.classList.add('estop-mode');
    resetAllToggles();
  } else {
    eStopBtn.classList.remove('active');
    document.body.classList.remove('estop-mode');
  }
}
eStopBtn.addEventListener('touchstart', function(e){ e.preventDefault(); activateEStop(); }, { passive: false });
eStopBtn.addEventListener('click', activateEStop);

//  MOVEMENT BUTTONS 
var moveIds = ['forward','backward','left','right'];
for(var i = 0; i < moveIds.length; i++){
  (function(id){
    var btn = document.getElementById(id);
    btn.addEventListener('touchstart', function(e){
      e.preventDefault();
      if(emergencyStop) return;
      btn.classList.add('pressing');
      fetch('/' + id);
    }, { passive: false });
    btn.addEventListener('touchend', function(){
      btn.classList.remove('pressing');
      if(!emergencyStop) fetch('/stop');
    });
    btn.addEventListener('touchcancel', function(){
      btn.classList.remove('pressing');
      if(!emergencyStop) fetch('/stop');
    });
    btn.addEventListener('mousedown', function(){
      if(emergencyStop) return;
      btn.classList.add('pressing');
      fetch('/' + id);
    });
    btn.addEventListener('mouseup', function(){
      btn.classList.remove('pressing');
      if(!emergencyStop) fetch('/stop');
    });
  })(moveIds[i]);
}

//  TOGGLE SWITCHES 
// redLight   sends /redLight?state=true/false  Arduino blinks RED LED
// whiteLight  sends /whiteLight?state=true/false  Arduino turns WHITE LED solid ON/OFF
var switchIds = ['redLight','whiteLight','mode1','mode2'];
for(var j = 0; j < switchIds.length; j++){
  (function(id){
    var btn = document.getElementById(id);
    function doToggle(){
      if(emergencyStop) return;
      toggleStates[id] = !toggleStates[id];
      if(toggleStates[id]) btn.classList.add('on');
      else                 btn.classList.remove('on');
      fetch('/' + id + '?state=' + toggleStates[id]);
    }
    btn.addEventListener('touchstart', function(e){ e.preventDefault(); doToggle(); }, { passive: false });
    btn.addEventListener('click', doToggle);
  })(switchIds[j]);
}
</script>

</body>
</html>
)=====";

// ===== MOTOR FUNCTIONS =====
void forward(){
  analogWrite(ENA, driveSpeed);
  analogWrite(ENB, driveSpeed);
  digitalWrite(IN1,LOW); digitalWrite(IN2,HIGH);
  digitalWrite(IN3,LOW); digitalWrite(IN4,HIGH);
}

void backward(){
  analogWrite(ENA, driveSpeed);
  analogWrite(ENB, driveSpeed);
  digitalWrite(IN1,HIGH); digitalWrite(IN2,LOW);
  digitalWrite(IN3,HIGH); digitalWrite(IN4,LOW);
}

void stopCar(){
  digitalWrite(IN1,LOW); digitalWrite(IN2,LOW);
  digitalWrite(IN3,LOW); digitalWrite(IN4,LOW);
}

void left(){
  analogWrite(ENA, turnSpeed / 3);
  analogWrite(ENB, turnSpeed);
  digitalWrite(IN1,HIGH); digitalWrite(IN2,LOW);
  digitalWrite(IN3,LOW); digitalWrite(IN4,HIGH);
}

void right(){
  analogWrite(ENA, turnSpeed);
  analogWrite(ENB, turnSpeed / 3);
  digitalWrite(IN1,LOW); digitalWrite(IN2,HIGH);
  digitalWrite(IN3,HIGH); digitalWrite(IN4,LOW);
}

// ===== LIGHTS =====

// PARKING  sets parkingOn flag; blinking is handled in loop()
void handleRedLight(){
  if(server.arg("state") == "true"){
    parkingOn = true;
  } else {
    parkingOn    = false;
    redLedState  = false;
    // RED_LED is ACTIVE HIGH: LOW = OFF
    digitalWrite(RED_LED, LOW);
  }
  server.send(200, "text/plain", "OK");
}

// WHITE LIGHTS  solid ON/OFF
void handleWhiteLight(){
  if(server.arg("state") == "true") digitalWrite(WHITE_LED, HIGH);
  else                              digitalWrite(WHITE_LED, LOW);
  server.send(200, "text/plain", "OK");
}

// ===== MODES =====
void handleMode1(){
  if(server.arg("state") == "true") driveSpeed = 1023;
  else                              driveSpeed = 512;
  server.send(200, "text/plain", "OK");
}

void handleMode2(){
  if(server.arg("state") == "true") turnSpeed = 1023;
  else                              turnSpeed = 512;
  server.send(200, "text/plain", "OK");
}

// ===== SETUP =====
void setup(){
  pinMode(IN1,OUTPUT); pinMode(IN2,OUTPUT);
  pinMode(IN3,OUTPUT); pinMode(IN4,OUTPUT);
  pinMode(ENA,OUTPUT); pinMode(ENB,OUTPUT);
  pinMode(RED_LED,OUTPUT);
  pinMode(WHITE_LED,OUTPUT);

  stopCar();
  digitalWrite(RED_LED,   LOW);    // OFF (active-high, so LOW = OFF)
  digitalWrite(WHITE_LED, LOW);    // OFF

  analogWrite(ENA, driveSpeed);
  analogWrite(ENB, driveSpeed);

  WiFi.softAP(ssid, password);

  server.on("/",          HTTP_GET, []{ server.send(200,"text/html",webpage); });
  server.on("/forward",   HTTP_GET, []{ forward();  server.send(200,"text/plain","OK"); });
  server.on("/backward",  HTTP_GET, []{ backward(); server.send(200,"text/plain","OK"); });
  server.on("/left",      HTTP_GET, []{ left();     server.send(200,"text/plain","OK"); });
  server.on("/right",     HTTP_GET, []{ right();    server.send(200,"text/plain","OK"); });
  server.on("/stop",      HTTP_GET, []{ stopCar();  server.send(200,"text/plain","OK"); });
  server.on("/redLight",   HTTP_GET, handleRedLight);
  server.on("/whiteLight", HTTP_GET, handleWhiteLight);
  server.on("/mode1",      HTTP_GET, handleMode1);
  server.on("/mode2",      HTTP_GET, handleMode2);

  server.begin();
}

// ===== LOOP =====
void loop(){
  server.handleClient();

  //  HAZARD BLINK for RED LED (PARKING mode) 
  // RED_LED is ACTIVE HIGH: HIGH = ON, LOW = OFF
  if(parkingOn){
    unsigned long now = millis();
    if(now - lastBlinkTime >= BLINK_INTERVAL){
      lastBlinkTime = now;
      redLedState = !redLedState;
      // Active-high: when redLedState=true  write HIGH (ON)
      digitalWrite(RED_LED, redLedState ? HIGH : LOW);
    }
  }
}

Credits

HumanixTechLab
6 projects • 0 followers
Independent maker turning ideas into real-world innovations through electronics, robotics, embedded systems & open hardware.

Comments