NAV — a Screenless Haptic Navigation Band for a Safer Future

by PriyankTyagi in Circuits > Microcontrollers

239 Views, 5 Favorites, 0 Comments

NAV — a Screenless Haptic Navigation Band for a Safer Future

ChatGPT Image Jun 8, 2026, 03_24_21 PM.png
IMG-20260531-WA0037.jpg
WhatsApp Image 2026-05-31 at 10.04.58 PM.jpeg
WhatsApp Image 2026-05-31 at 10.01.30 PM.jpeg
WhatsApp Image 2026-05-31 at 10.01.29 PM.jpeg

Every year, thousands of pedestrians, cyclists, and drivers are injured because they looked down at their phone for directions. Delivery drivers spend 8+ hours a day glancing at navigation apps. Cyclists take their eyes off the road at the worst moments. Pedestrians walk into traffic staring at a blue dot on a map.

The problem isn't navigation — it's the screen.

NAV is a wrist-worn navigation device that guides you turn-by-turn using directional LEDs — no screen, no headphones, no distraction. It connects to a custom SmartNavWatch web app over WiFi and WebSockets, which sends real-time directions straight to your wrist. The correct LED lights up — Left, Right, Straight, Back or Destination — and you simply follow the light.

Built around the ESP32-C3 microcontroller, NAV was successfully tested on a real 8.7km route from Dehradun to Haridwar — fully functional on real roads, real turns, real conditions.

Features

  1. 4 directional LEDs — Left, Right, Straight, Destination
  2. OLED display showing current direction and distance
  3. Capacitive touch input for interaction
  4. WiFi + WebSocket real-time communication
  5. SmartNavWatch web app with live map, ETA, and GPS

Who It's For

  1. Pedestrians tired of staring at their phone
  2. Cyclists who need both eyes on the road
  3. Delivery drivers navigating unknown streets all day
  4. Visually impaired users needing affordable navigation
  5. Hikers and outdoor adventurers
  6. Riders who need both hands on the handlebars
"No screen. No distraction. Just NAV."

Supplies

ChatGPT Image Jun 8, 2026, 07_53_29 PM.png

ComponentDetails

  1. ESP32-C3 Microcontroller - Main brain of the device
  2. OLED Display - 0.96 inch, I2C, SSD1306
  3. LEDs - 4x clear LEDs — direction indicators
  4. Vibration Motors - For haptic feedback
  5. TP4056 Charging Module - LiPo battery charging circuit
  6. LiPo Battery - 3.7V, 700mAh
  7. Slide Switch - Power on/off
  8. 3D Printed Case - Custom enclosure
  9. Back Cover - With screws for assembly
  10. Velcro Strap - Comfortable watch-style wrist band

Tools Needed

  1. Soldering iron + solder
  2. Hot glue gun
  3. Small screwdriver
  4. Wire stripper
  5. USB-C cable (for programming)
  6. 3D printer (or use a local print service)

Software

  1. Arduino IDE 2.3.9
  2. SmartNavWatch Web App (provided in files)

Arduino Libraries

  1. Adafruit SSD1306 : 2.5.17
  2. Adafruit GFX : 1.12.6
  3. WebSockets_Generic : 2.16.1
  4. ArduinoJson : 7.4.3

Setting Up Arduino IDE

WhatsApp Image 2026-05-31 at 10.04.49 PM (3).jpeg
WhatsApp Image 2026-05-31 at 10.04.49 PM (2).jpeg
WhatsApp Image 2026-05-31 at 10.04.49 PM (4).jpeg
WhatsApp Image 2026-05-31 at 10.04.49 PM (5).jpeg
WhatsApp Image 2026-05-31 at 10.04.49 PM (7).jpeg
WhatsApp Image 2026-05-31 at 10.04.50 PM (3).jpeg
WhatsApp Image 2026-05-31 at 10.04.50 PM (2).jpeg
WhatsApp Image 2026-05-31 at 10.04.50 PM (1).jpeg
WhatsApp Image 2026-05-31 at 10.04.50 PM.jpeg

Before writing any code, we need to prepare Arduino IDE to work with the ESP32-C3 microcontroller.

1A. Add ESP32 Board URL

  1. Open Arduino IDE
  2. Go to File → Preferences
  3. Find "Additional Boards Manager URLs"
  4. Click the icon next to the field and add this URL:


https://espressif.github.io/arduino-esp32/package_esp32_index.json
  1. Click OK


1B. Install ESP32 Board Package

  1. Go to Tools → Boards Manager
  2. Search esp32
  3. Find "esp32 by Espressif Systems"
  4. Install version 3.3.8
  5. Wait for installation to complete


1C. Select Your Board

  1. Go to Tools → Board → esp32
  2. Select ESP32C3 Dev Module

1D. Install Required Libraries

Go to Tools → Manage Libraries and install these one by one:

  1. Adafruit SSD1306 : 2.5.17
  2. Adafruit GFX : 1.12.6
  3. WebSockets_Generic : 2.16.1
  4. ArduinoJson : 7.4.3


Wiring the Circuit

circuit.png

This is the most important step. Follow the pin definitions exactly as used in the code.

Pin Definitions

#define OLED_SDA 8
#define OLED_SCL 9
#define LED_LEFT 0
#define LED_RIGHT 1
#define LED_STRAIGHT 2
#define LED_DESTINATION 3
#define TOUCH_PIN 4

Wiring Circuit

[ refer to image 1]

Power Circuit

LiPo Battery (+) → Slide Switch → TP4056 IN+
TP4056 OUT+ → ESP32-C3 3.3V
LiPo Battery (-) → GND
⚠️ Warning: Always double-check polarity before connecting the LiPo battery. Reverse polarity can damage the ESP32-C3 permanently.
💡 Tip: Solder all connections and use heat shrink tubing — loose wires inside a small enclosure will cause random disconnections.

The Code

The NAV firmware runs a WiFi WebSocket server on the ESP32-C3. The SmartNavWatch web app connects to it and sends direction commands — the ESP32-C3 reads them and lights the correct LED.

3A. How It Works


Phone (SmartNavWatch app)
↓ WiFi + WebSocket
ESP32-C3 receives command
LEFT / RIGHT / STRAIGHT / DESTINATION
Correct LED lights up on wrist


3B. Full Code


#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
#include <WebSocketsServer.h>
#include <LittleFS.h>
#include <ArduinoJson.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Update.h>

const char* WIFI_SSID = "YOUR_WIFI_NAME";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";

#define OLED_SDA 8
#define OLED_SCL 9
#define LED_LEFT 0
#define LED_RIGHT 1
#define LED_STRAIGHT 2
#define LED_DESTINATION 3
#define TOUCH_PIN 4

#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define OLED_ADDR 0x3C

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
WebServer httpServer(80);
WebSocketsServer wsServer(81);

struct NavData {
char direction[16] = "NONE";
char distance[16] = "---";
char eta[16] = "---";
char destination[32] = "---";
char streetName[32] = "---";
float lat = 0.0f;
float lng = 0.0f;
float bearing = 0.0f;
float totalDist = 0.0f;
bool gpsActive = false;
bool navigating = false;
} nav;

struct ClockData {
uint8_t hour = 0;
uint8_t minute = 0;
uint8_t second = 0;
uint8_t day = 1;
uint8_t month = 1;
uint16_t year = 2026;
char dayName[8] = "MON";
char monthName[8] = "JAN";
bool synced = false;
} clk;

struct SysState {
bool wifiOK = false;
bool wsOK = false;
uint8_t battery = 0;
bool use24h = true;
uint8_t screen = 0;
char ip[20] = "---";
int8_t rssi = -100;
} sys;

struct TouchSt {
bool last = false;
bool held = false;
uint32_t pressStart = 0;
uint32_t lastTap = 0;
uint8_t taps = 0;
bool longFired = false;
bool processed = false;
} touch;

#define DEBOUNCE_MS 50
#define DOUBLE_TAP_MS 400
#define LONG_PRESS_MS 800
#define OLED_MS 100
#define LED_MS 120
#define WIFI_MS 8000
#define COLON_MS 500
#define ARROW_MS 600

uint32_t tOLED=0, tLED=0, tWiFi=0, tColon=0, tArrow=0;
uint8_t arrowFr=0;
bool colonOn=true;

static const char* MONTHS[]={"JAN","FEB","MAR","APR","MAY","JUN","JUL","AUG","SEP","OCT","NOV","DEC"};
static const char* DAYS[] ={"SUN","MON","TUE","WED","THU","FRI","SAT"};

void setupOLED();
void setupLEDs();
void setupWiFi();
void setupHTTP();
void setupWS();
void loopOLED();
void loopTouch();
void loopLEDs();
void loopWiFi();
void drawClock();
void drawNav();
void drawStatus();
void drawBoot(uint8_t phase);
void drawArrowShape(int cx, int cy, const char* dir, uint8_t frame);
void drawBattery(int x, int y, uint8_t pct);
void drawWiFiBars(int x, int y, int8_t rssi);
void drawBar(int x, int y, int w, int h, uint8_t pct);
void allOff();
void setLEDs(const char* dir);
void pulseDest();
void onSingleTap();
void onDoubleTap();
void onLongPress();
void onWsEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length);
void handleMsg(uint8_t* payload, size_t length);
void broadcastStatus();
void serveFile(const char* path, const char* mime);
void buildStatusJSON(char* buf, size_t sz);

void setup() {
Serial.begin(115200);
Wire.begin(OLED_SDA, OLED_SCL);
setupOLED();
setupLEDs();
pinMode(TOUCH_PIN, INPUT);
drawBoot(0);
if (!LittleFS.begin(true)) Serial.println(F("[FS] FAILED"));
drawBoot(1);
setupWiFi();
drawBoot(2);
setupHTTP();
setupWS();
drawBoot(3);
}

void loop() {
httpServer.handleClient();
wsServer.loop();
loopTouch();
loopOLED();
loopLEDs();
loopWiFi();
}

void setupOLED() {
if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) return;
display.clearDisplay();
display.setRotation(0);
display.ssd1306_command(SSD1306_SETCONTRAST);
display.ssd1306_command(180);
display.display();
}

void setupLEDs() {
const uint8_t pins[]={LED_LEFT,LED_RIGHT,LED_STRAIGHT,LED_DESTINATION};
for (uint8_t p:pins) { pinMode(p,OUTPUT); digitalWrite(p,LOW); }
for (uint8_t p:pins) { digitalWrite(p,HIGH); delay(60); digitalWrite(p,LOW); delay(30); }
}

void setupWiFi() {
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
WiFi.setAutoReconnect(true);
WiFi.persistent(true);
uint32_t t=millis();
while (WiFi.status()!=WL_CONNECTED && millis()-t<15000) { delay(300); Serial.print('.'); }
if (WiFi.status()==WL_CONNECTED) {
sys.wifiOK=true;
strlcpy(sys.ip, WiFi.localIP().toString().c_str(), sizeof(sys.ip));
sys.rssi=WiFi.RSSI();
} else {
WiFi.mode(WIFI_AP);
WiFi.softAP("SmartNavWatch","smartnav123");
strlcpy(sys.ip, WiFi.softAPIP().toString().c_str(), sizeof(sys.ip));
sys.wifiOK=true;
}
}

void setupHTTP() {
httpServer.on("/", HTTP_GET, []() { serveFile("/index.html","text/html"); });
httpServer.onNotFound([]() {
String path=httpServer.uri();
if (LittleFS.exists(path)) {
String mime="text/plain";
if (path.endsWith(".html")) mime="text/html";
else if (path.endsWith(".css")) mime="text/css";
else if (path.endsWith(".js")) mime="application/javascript";
else if (path.endsWith(".json")) mime="application/json";
File f=LittleFS.open(path,"r");
httpServer.streamFile(f,mime);
f.close();
} else { serveFile("/index.html","text/html"); }
});
httpServer.on("/api/status", HTTP_GET, []() {
char buf[256]; buildStatusJSON(buf,sizeof(buf));
httpServer.send(200,"application/json",buf);
});
httpServer.on("/ota", HTTP_POST,
[]() { httpServer.send(200,"text/plain",Update.hasError()?"FAIL":"OK"); delay(500); ESP.restart(); },
[]() {
HTTPUpload& up=httpServer.upload();
if (up.status==UPLOAD_FILE_START) { if (!Update.begin(UPDATE_SIZE_UNKNOWN)) Update.printError(Serial); }
else if (up.status==UPLOAD_FILE_WRITE) { if (!Update.write(up.buf,up.currentSize)) Update.printError(Serial); }
else if (up.status==UPLOAD_FILE_END) { if (Update.end(true)) Serial.printf("[OTA] OK %u bytes\n",up.totalSize); }
}
);
httpServer.begin();
}

void setupWS() {
wsServer.begin();
wsServer.onEvent(onWsEvent);
}

void loopOLED() {
uint32_t now=millis();
if (now-tOLED<OLED_MS) return;
tOLED=now;
if (now-tColon>=COLON_MS) { colonOn=!colonOn; tColon=now; }
if (now-tArrow>=ARROW_MS/4) { arrowFr=(arrowFr+1)%4; tArrow=now; }
display.clearDisplay();
switch (sys.screen) {
case 0: drawClock(); break;
case 1: drawNav(); break;
case 2: drawStatus(); break;
default: sys.screen=0;
}
display.display();
}

void loopTouch() {
uint32_t now=millis();
bool raw=digitalRead(TOUCH_PIN);
static uint32_t lastDb=0;
if (raw!=touch.last) lastDb=now;
if (now-lastDb<DEBOUNCE_MS) return;
touch.last=raw;
if (raw && !touch.held) { touch.held=true; touch.pressStart=now; touch.longFired=false; touch.processed=false; }
if (raw && touch.held && !touch.longFired && !touch.processed) {
if (now-touch.pressStart>=LONG_PRESS_MS) { touch.longFired=true; touch.processed=true; onLongPress(); }
}
if (!raw && touch.held) { touch.held=false; if (!touch.processed) { touch.taps++; touch.lastTap=now; } }
if (touch.taps>0 && !touch.held) {
if (now-touch.lastTap>=DOUBLE_TAP_MS) {
if (touch.taps==1) onSingleTap(); else onDoubleTap();
touch.taps=0;
}
}
}

void loopLEDs() {
uint32_t now=millis();
if (now-tLED<LED_MS) return;
tLED=now;
if (nav.navigating) { setLEDs(nav.direction); if (strcmp(nav.direction,"ARRIVED")==0) pulseDest(); }
else allOff();
}

void loopWiFi() {
uint32_t now=millis();
if (now-tWiFi<WIFI_MS) return;
tWiFi=now;
if (WiFi.getMode()!=WIFI_STA) return;
if (WiFi.status()!=WL_CONNECTED) { sys.wifiOK=false; WiFi.reconnect(); }
else { sys.wifiOK=true; sys.rssi=WiFi.RSSI(); }
}

void drawClock() {
drawWiFiBars(114,2,sys.rssi);
drawBattery(96,2,sys.battery);
if (nav.gpsActive) display.fillCircle(86,5,2,SSD1306_WHITE);
char tbuf[10];
if (sys.use24h) snprintf(tbuf,sizeof(tbuf),"%02d%c%02d",clk.hour,colonOn?':':' ',clk.minute);
else { uint8_t h=clk.hour%12; if(!h)h=12; snprintf(tbuf,sizeof(tbuf),"%d%c%02d",h,colonOn?':':' ',clk.minute); }
display.setTextSize(4);
int16_t x1,y1; uint16_t w,h;
display.getTextBounds(tbuf,0,0,&x1,&y1,&w,&h);
display.setCursor((128-w)/2,16); display.print(tbuf);
drawBar(14,48,100,2,(clk.second*100)/60);
char dbuf[24];
snprintf(dbuf,sizeof(dbuf),"%s, %02d %s",clk.dayName,clk.day,clk.monthName);
display.setTextSize(1);
display.getTextBounds(dbuf,0,0,&x1,&y1,&w,&h);
display.setCursor((128-w)/2,54); display.print(dbuf);
}

void drawNav() {
int16_t x1,y1; uint16_t w,h;
if (!nav.navigating) {
display.setTextSize(1);
display.getTextBounds("READY TO",0,0,&x1,&y1,&w,&h);
display.setCursor((128-w)/2,20); display.print("READY TO");
display.setTextSize(2);
display.getTextBounds("NAVIGATE",0,0,&x1,&y1,&w,&h);
display.setCursor((128-w)/2,34); display.print("NAVIGATE");
return;
}
display.fillRect(0,0,128,13,SSD1306_WHITE);
display.setTextColor(SSD1306_BLACK);
display.setTextSize(1);
char sn[22];
strlcpy(sn,nav.streetName[0]&&strcmp(nav.streetName,"---")!=0?nav.streetName:nav.destination,sizeof(sn));
display.getTextBounds(sn,0,0,&x1,&y1,&w,&h);
display.setCursor((128-w)/2,3); display.print(sn);
display.setTextColor(SSD1306_WHITE);
drawArrowShape(30,32,nav.direction,arrowFr);
display.setTextSize(2);
display.getTextBounds(nav.distance,0,0,&x1,&y1,&w,&h);
display.setCursor(128-w-4,18); display.print(nav.distance);
display.setTextSize(1);
char etabuf[24];
snprintf(etabuf,sizeof(etabuf),"ETA:%s",nav.eta);
display.getTextBounds(etabuf,0,0,&x1,&y1,&w,&h);
display.setCursor(128-w-4,38); display.print(etabuf);
display.drawLine(0,50,128,50,SSD1306_WHITE);
char action[32]="";
if (strcmp(nav.direction,"LEFT")==0||strcmp(nav.direction,"TURN_LEFT")==0) strcpy(action,"TURN LEFT");
else if (strcmp(nav.direction,"RIGHT")==0||strcmp(nav.direction,"TURN_RIGHT")==0) strcpy(action,"TURN RIGHT");
else if (strcmp(nav.direction,"STRAIGHT")==0||strcmp(nav.direction,"CONTINUE")==0) strcpy(action,"CONTINUE STRAIGHT");
else if (strcmp(nav.direction,"UTURN")==0) strcpy(action,"MAKE U-TURN");
else if (strcmp(nav.direction,"ARRIVED")==0) strcpy(action,"ARRIVED AT DEST");
else strcpy(action,"PROCEED");
display.getTextBounds(action,0,0,&x1,&y1,&w,&h);
display.setCursor((128-w)/2,54); display.print(action);
}

void drawStatus() {
int16_t x1,y1; uint16_t w,h;
display.fillRect(0,0,128,13,SSD1306_WHITE);
display.setTextColor(SSD1306_BLACK);
display.setTextSize(1);
display.getTextBounds("SYSTEM STATUS",0,0,&x1,&y1,&w,&h);
display.setCursor((128-w)/2,3); display.print("SYSTEM STATUS");
display.setTextColor(SSD1306_WHITE);
display.setCursor(4,18); display.print(F("WIFI:")); display.setCursor(40,18); display.print(sys.wifiOK?F("OK"):F("ERR"));
display.setCursor(4,28); display.print(F("GPS :")); display.setCursor(40,28); display.print(nav.gpsActive?F("FIX"):F("WAIT"));
display.setCursor(4,38); display.print(F("APP :")); display.setCursor(40,38); display.print(sys.wsOK?F("SYNCED"):F("DISC"));
display.drawLine(0,50,128,50,SSD1306_WHITE);
display.getTextBounds(sys.ip,0,0,&x1,&y1,&w,&h);
display.setCursor((128-w)/2,54); display.print(sys.ip);
drawBattery(106,18,sys.battery);
drawWiFiBars(112,28,sys.rssi);
}

void drawBoot(uint8_t phase) {
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
int16_t x1,y1; uint16_t w,h;
display.setTextSize(2);
display.getTextBounds("SMART NAV",0,0,&x1,&y1,&w,&h);
display.setCursor((128-w)/2,8); display.print("SMART NAV");
display.setTextSize(1);
display.drawLine(10,26,118,26,SSD1306_WHITE);
switch(phase) {
case 0: display.setCursor(34,34); display.print(F("OS BOOTING")); drawBar(14,50,100,4,10); break;
case 1: display.setCursor(22,34); display.print(F("MOUNTING FILES")); drawBar(14,50,100,4,40); break;
case 2:
display.setCursor(16,32); display.print(F("CONNECTING WIFI"));
display.setCursor((128-strlen(WIFI_SSID)*6)/2,42); display.print(WIFI_SSID);
drawBar(14,54,100,4,75); break;
default:
display.setCursor(28,32); display.print(F("SYSTEM READY"));
display.getTextBounds(sys.ip,0,0,&x1,&y1,&w,&h);
display.setCursor((128-w)/2,46); display.print(sys.ip); break;
}
display.display();
delay(phase==0?800:(phase==3?1500:400));
}

void drawArrowShape(int cx, int cy, const char* dir, uint8_t frame) {
static const int8_t ao[]={0,1,2,1};
int anim=ao[frame&3], sz=12;
if (strcmp(dir,"LEFT")==0||strcmp(dir,"TURN_LEFT")==0) {
cx-=anim;
display.fillTriangle(cx-sz,cy,cx+2,cy-sz,cx+2,cy+sz,SSD1306_WHITE);
display.fillRect(cx+2,cy-sz/2,sz-2,sz,SSD1306_WHITE);
} else if (strcmp(dir,"RIGHT")==0||strcmp(dir,"TURN_RIGHT")==0) {
cx+=anim;
display.fillTriangle(cx+sz,cy,cx-2,cy-sz,cx-2,cy+sz,SSD1306_WHITE);
display.fillRect(cx-sz,cy-sz/2,sz-2,sz,SSD1306_WHITE);
} else if (strcmp(dir,"STRAIGHT")==0||strcmp(dir,"CONTINUE")==0) {
cy-=anim;
display.fillTriangle(cx,cy-sz,cx-sz,cy+2,cx+sz,cy+2,SSD1306_WHITE);
display.fillRect(cx-sz/2,cy+2,sz,sz-2,SSD1306_WHITE);
} else if (strcmp(dir,"UTURN")==0) {
display.drawCircle(cx,cy+4,sz/2,SSD1306_WHITE);
display.fillCircle(cx,cy+4,sz/2-3,SSD1306_BLACK);
display.fillRect(cx-sz/2,cy+4,sz+1,sz,SSD1306_BLACK);
display.fillTriangle(cx+sz/2-3,cy+1-anim,cx+sz/2+3,cy+1-anim,cx+sz/2,cy+6-anim,SSD1306_WHITE);
display.fillRect(cx-sz/2,cy+4,3,6,SSD1306_WHITE);
} else if (strcmp(dir,"ARRIVED")==0) {
display.fillCircle(cx,cy,sz-2,SSD1306_WHITE);
display.fillCircle(cx,cy,sz-4,SSD1306_BLACK);
display.fillCircle(cx,cy,3,SSD1306_WHITE);
} else {
display.setTextSize(2); display.setCursor(cx-5,cy-7); display.print('?');
}
}

void drawBattery(int x, int y, uint8_t pct) {
display.drawRect(x,y,14,7,SSD1306_WHITE);
display.drawRect(x+14,y+2,2,3,SSD1306_WHITE);
uint8_t f=map(constrain(pct,0,100),0,100,0,12);
if(f) display.fillRect(x+1,y+1,f,5,SSD1306_WHITE);
}

void drawWiFiBars(int x, int y, int8_t rssi) {
int b=0;
if (rssi>-55) b=3; else if (rssi>-70) b=2; else if (rssi>-85) b=1;
if (b>=1) display.fillRect(x, y+4,3,3,SSD1306_WHITE);
if (b>=2) display.fillRect(x+4,y+2,3,5,SSD1306_WHITE);
if (b>=3) display.fillRect(x+8,y, 3,7,SSD1306_WHITE);
}

void drawBar(int x, int y, int w, int h, uint8_t pct) {
display.drawRect(x,y,w,h,SSD1306_WHITE);
uint8_t f=map(constrain(pct,0,100),0,100,0,w-2);
if(f) display.fillRect(x+1,y+1,f,h-2,SSD1306_WHITE);
}

void onSingleTap() { sys.screen=(sys.screen+1)%3; }

void onDoubleTap() {
static uint8_t lvl=2;
static const uint8_t lv[]={50,128,200,255};
lvl=(lvl+1)&3;
display.ssd1306_command(SSD1306_SETCONTRAST);
display.ssd1306_command(lv[lvl]);
}

void onLongPress() {
if (nav.navigating) { nav.navigating=false; strlcpy(nav.direction,"NONE",sizeof(nav.direction)); allOff(); }
else sys.screen=1;
}

void allOff() {
digitalWrite(LED_LEFT,LOW); digitalWrite(LED_RIGHT,LOW);
digitalWrite(LED_STRAIGHT,LOW); digitalWrite(LED_DESTINATION,LOW);
}

void setLEDs(const char* dir) {
allOff();
if (strcmp(dir,"LEFT")==0 ||strcmp(dir,"TURN_LEFT")==0) digitalWrite(LED_LEFT,HIGH);
else if (strcmp(dir,"RIGHT")==0 ||strcmp(dir,"TURN_RIGHT")==0) digitalWrite(LED_RIGHT,HIGH);
else if (strcmp(dir,"STRAIGHT")==0||strcmp(dir,"CONTINUE")==0) digitalWrite(LED_STRAIGHT,HIGH);
else if (strcmp(dir,"ARRIVED")==0) digitalWrite(LED_DESTINATION,HIGH);
}

void pulseDest() {
static bool st=false;
static uint32_t last=0;
if (millis()-last>300) { st=!st; digitalWrite(LED_DESTINATION,st?HIGH:LOW); last=millis(); }
}

void serveFile(const char* path, const char* mime) {
if (LittleFS.exists(path)) { File f=LittleFS.open(path,"r"); httpServer.streamFile(f,mime); f.close(); }
else httpServer.send(404,"text/plain","Not found");
}

void buildStatusJSON(char* buf, size_t sz) {
snprintf(buf,sz,
"{\"type\":\"status\",\"wifi\":%s,\"ws\":%s,\"gps\":%s,"
"\"ip\":\"%s\",\"screen\":%d,\"battery\":%d,"
"\"navigating\":%s,\"direction\":\"%s\",\"rssi\":%d}",
sys.wifiOK?"true":"false", sys.wsOK?"true":"false",
nav.gpsActive?"true":"false", sys.ip, sys.screen,
sys.battery, nav.navigating?"true":"false",
nav.direction, sys.rssi);
}

void onWsEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length) {
switch(type) {
case WStype_CONNECTED: {
sys.wsOK=true;
char buf[256]; buildStatusJSON(buf,sizeof(buf));
wsServer.sendTXT(num,buf); break;
}
case WStype_DISCONNECTED: sys.wsOK=false; break;
case WStype_TEXT: handleMsg(payload,length); break;
default: break;
}
}

void handleMsg(uint8_t* payload, size_t length) {
DynamicJsonDocument doc(512);
if (deserializeJson(doc,(const char*)payload,length)) return;
const char* type=doc["type"]|"";
if (strcmp(type,"nav")==0) {
if (doc.containsKey("direction")) strlcpy(nav.direction, doc["direction"] |"NONE",sizeof(nav.direction));
if (doc.containsKey("distance")) strlcpy(nav.distance, doc["distance"] |"---", sizeof(nav.distance));
if (doc.containsKey("eta")) strlcpy(nav.eta, doc["eta"] |"---", sizeof(nav.eta));
if (doc.containsKey("destination")) strlcpy(nav.destination,doc["destination"]|"---", sizeof(nav.destination));
if (doc.containsKey("street")) strlcpy(nav.streetName, doc["street"] |"---", sizeof(nav.streetName));
nav.bearing =doc["bearing"] |nav.bearing;
nav.totalDist =doc["totalDist"] |nav.totalDist;
nav.navigating=doc["navigating"]|nav.navigating;
if (nav.navigating) sys.screen=1;
} else if (strcmp(type,"time")==0) {
clk.hour=doc["hour"]|0; clk.minute=doc["min"]|0; clk.second=doc["sec"]|0;
clk.day=doc["day"]|1; clk.month=doc["month"]|1; clk.year=doc["year"]|2026;
int wd=doc["wday"]|0;
if (clk.month>=1&&clk.month<=12) strlcpy(clk.monthName,MONTHS[clk.month-1],sizeof(clk.monthName));
if (wd>=0&&wd<=6) strlcpy(clk.dayName, DAYS[wd], sizeof(clk.dayName));
clk.synced=true;
} else if (strcmp(type,"settings")==0) {
if (doc.containsKey("oledBrightness")) {
display.ssd1306_command(SSD1306_SETCONTRAST);
display.ssd1306_command((uint8_t)doc["oledBrightness"]);
}
if (doc.containsKey("use24h")) sys.use24h =doc["use24h"];
if (doc.containsKey("battery")) sys.battery=doc["battery"];
} else if (strcmp(type,"gps")==0) {
nav.lat=doc["lat"]|0.0f; nav.lng=doc["lng"]|0.0f; nav.gpsActive=true;
} else if (strcmp(type,"ping")==0) {
wsServer.broadcastTXT("{\"type\":\"pong\"}"); return;
}
broadcastStatus();
}

void broadcastStatus() {
char buf[256];
buildStatusJSON(buf,sizeof(buf));
wsServer.broadcastTXT(buf);
}


3C. What to Change

Before uploading, update these two lines with your WiFi details:

const char* ssid = "YOUR_WIFI_NAME";
const char* password = "YOUR_WIFI_PASSWORD";
⚠️ Note: The ESP32-C3 and your phone must be on the same WiFi network for the WebSocket connection to work.


3D. Upload to ESP32-C3

  1. Connect ESP32-C3 via USB-C
  2. Select ESP32C3 Dev Module in board selector
  3. Select correct COM Port under Tools → Port
  4. Click Verify ✓ — fix any errors
  5. Click Upload → — wait for "Done uploading"
  6. Open Serial Monitor at 115200 baud
  7. You should see the IP address printed


💡 Tip: Note down the IP address shown in Serial Monitor — you'll need it for the SmartNavWatch web app in the next step.

The SmartNavWatch Web App

WEB.gif
WhatsApp Image 2026-05-31 at 10.04.49 PM.jpeg

The SmartNavWatch is a browser-based web app that runs on your phone. It calculates your route, sends real-time turn-by-turn directions to NAV over WiFi, and shows a live map with ETA and distance.

4A. How It Works


Enter destination on phone
App calculates route using OpenRouteService API
App detects your GPS position in real time
Sends direction command to NAV via WebSocket
NAV lights correct LED on your wrist

4B. Setting It Up

Step 1 — Get a free API key:

  1. Go to openrouteservice.org
  2. Create a free account
  3. Copy your API key

Step 2 — Open the web app:

  1. Open SmartNavWatch.html from the attached files
  2. Find this line near the top:
const ORS_API_KEY = "YOUR_API_KEY_HERE";
  1. Replace with your actual API key

Step 3 — Enter your NAV device IP:

  1. Open Serial Monitor in Arduino IDE
  2. Note the IP address shown (e.g. 192.168.1.105)
  3. Enter it in the app's IP field

4C. Using the App

  1. Open SmartNavWatch.html in your phone browser
  2. Enter the NAV device IP address
  3. Click Connect — status shows 🟢 Live
  4. Type your destination and click Start
  5. The live map appears with your route in cyan
  6. NAV starts guiding you immediately


4D. What the App Shows

  1. Direction arrow : Current turn with distance
  2. ETA : Estimated time to destination
  3. Distance : Total remaining distance
  4. GPS coordinates : Your live position
  5. Live map : Full route with cyan line
  6. Stop button : Ends navigation

4E. Direction Commands Sent to NAV

App sends NAV does

LEFT LED Left glows

RIGHT LED Right glows

STRAIGHT LED Straight glows

DESTINATION LED All LED glows

💡 Tip: Keep the SmartNavWatch tab open and screen on while navigating. The app needs GPS access — allow location permission when your browser asks.
⚠️ Note: Both your phone and NAV device must be connected to the same WiFi network.

Assembly — Building the Case

VID-20260608-WA0004 - REVERSE - ROTATE - Videobolt.net (2).gif
3D PRINTING.gif
3DPRINTING 2.gif

Now that the code is working, it's time to put everything inside the 3D printed case and assemble the final wearable.

5A. 3D Print the Case

The case has two parts:

  1. Main body — black, holds all electronics
  2. Back cover — white, screws on to close the case

Print settings:

Setting Value

Material PLA

Layer height 0.2mm

Infill 20%

Supports Yes

Print time ~2-3 hours

💡 Tip: Print the main body in black and back cover in white — exactly like the final build. It gives a clean two-tone look.


5B. Prepare the Components

Before assembling, solder and test everything on a flat surface:

  1. Solder 220Ω resistors to each LED leg (positive side)
  2. Solder wires to all LED resistor ends
  3. Solder wires to OLED display pins (SDA, SCL, VCC, GND)
  4. Solder wires to TP4056 charging module
  5. Solder slide switch between battery positive and circuit
  6. Solder vibration motor wires
  7. Test all connections before closing the case
⚠️ Warning: Test everything BEFORE assembly. Once the case is screwed shut, fixing mistakes is very difficult.

5C. Place Components Inside Case

Follow this order:

1. Place ESP32-C3 flat inside the case base

2. Push 4 LEDs through the holes on each side:

  1. Left hole → LED Left (GPIO 0)
  2. Right hole → LED Right (GPIO 1)
  3. Top hole → LED Straight (GPIO 2)
  4. Bottom hole → LED Back (GPIO 3)

3. Seat the OLED display in the front window cutout

4. Place TP4056 charging module at the bottom

5. Place LiPo battery flat — it fits snugly under the ESP32-C3

6. Route the slide switch to the side slot

7. Tuck the vibration motor in any remaining space


5D. Close and Secure

  1. Carefully fold all wires inside
  2. Place back cover over the case
  3. Align screw holes
  4. Insert and tighten all screws — don't overtighten on PLA
  5. Check all 4 LEDs are visible and not blocked

5E. Attach the Wrist Strap

  1. Thread the velcro strap through the lugs on both sides of the case
  2. Adjust to comfortable wrist size
  3. Press velcro to secure


💡 Tip: The elastic velcro strap is comfortable for all-day wear and keeps NAV snug on your wrist even during physical activity.

Testing NAV on a Real Route

DESTINATION.gif
FH895V5MPX8Q6XX.jpg
SOS.gif
WhatsApp Image 2026-05-31 at 10.14.18 PM.jpeg

This is the most exciting step — taking NAV outside and testing it on a real road.

6A. Before You Go Outside

Do a quick indoor test first:

  1. Turn on NAV using the slide switch
  2. Wait for OLED to show "NAV Ready" and the IP address
  3. Connect your phone to the same WiFi
  4. Open SmartNavWatch in your phone browser
  5. Enter the NAV IP address and click Connect
  6. Status should show 🟢 Live
  7. Manually send test commands and verify:

Command Expected Result

LEFT Left LED glows

RIGHT Right LED glows

STRAIGHT Straight LED glows

DESTINATION All LED glows

✅ If all 4 LEDs respond correctly — you're ready for the real test!

6B. Real World Test

Our test route:

  1. Start: Dehradun
  2. Destination: Haridwar
  3. Distance: 8.7 km
  4. ETA: 13 minutes


How to start navigation:

  1. Go outside with your phone and NAV on your wrist
  2. Make sure both are on the same WiFi hotspot
  3. Open SmartNavWatch on your phone
  4. Type your destination and tap Start
  5. The cyan route line appears on the live map
  6. Start walking or riding — NAV guides you with LEDs

6C. What Happens During Navigation


You approach a turn
SmartNavWatch detects your GPS position
Calculates next turn direction
Sends command to NAV via WebSocket
Correct LED lights up on your wrist
You turn — without looking at your phone

6D. Night Test

NAV is especially powerful at night.

The LEDs are extremely bright in low light — visible from a distance, making NAV useful not just for navigation but also for safety visibility.


💡 Tip: The night glow effect is one of NAV's best features — perfect for cyclists and night walkers who need both navigation AND visibility.

6E. Troubleshooting

Problem Fix

App won't connect Check both devices on same WiFi

Wrong LED lights up Recheck wiring table from Step 2

OLED shows nothing Check SDA/SCL pins (GPIO 8 & 9)

LEDs too dim Check 220Ω resistors are correct value

No GPS on app Allow location permission in browser

WebSocket drops Move phone closer to NAV device

Conclusion and Future Improvements

When the device is powered ON using the slide switch:

  1. The OLED display lights up
  2. A boot animation plays — "SMART NAV" appears with a loading bar
  3. The device connects to WiFi and displays the IP address
  4. The screen transitions into the Clock screen — showing live time and date
  5. A single tap switches to Navigation screen — ready to guide
  6. Another tap switches to System Status — WiFi, GPS, and connection state

Navigation has always demanded our eyes. From paper maps to glowing screens, we have never stopped looking down. NAV changes that.

Inspired by the idea that the best technology should be invisible, NAV transforms complex real-time navigation into something as simple as a light on your wrist. What once required a screen glowing in your face can now be understood in a single glance — or no glance at all.

By combining modern embedded technology with intuitive human instinct — the same instinct that makes us follow a pointed finger — NAV bridges the gap between where we are and where we need to go.

In a world full of notifications, alerts, and glowing rectangles competing for our attention, NAV asks for none of it.

A small device — pointing you in the right direction.

"This project was created as part of the Dream a Better World Contest."