⚠ ALERT|

Every 40 seconds, a child goes missing somewhere in the world. Could it be a child you know?

*Source: ICMEC / Missing Children global statistics. SENTRICK™ is an assistive tracking tool — not a substitute for parental supervision or emergency services.
SENTRICK
SENTRICK
ANIMA TECHNOLOGY™: SENSOR & TRACKING SOLUTIONS, LLC
Engineering Deep Dive

Building a Real-Time GPS + Camera IoT System: ESP32-S3-EYE + SparkFun NEO-M9N + Firebase

A technical walkthrough of the SENTRICK™ hardware core — from non-blocking GPS firmware design to live camera streaming and real-time Firebase cloud sync. Every line of code that powers the world's most complete tracking ecosystem.

May 11, 2026·12 min read·Anima Technology™·Firmware v2.3
ESP32-S3-EYESparkFun NEO-M9N GPSFirebase Realtime DBArduino FrameworkSENTRICK™ v2.3

The SENTRICK™ device is not a concept — it is a fully operational embedded system running on real hardware, tested in the field, and streaming live GPS coordinates and camera frames to Firebase every two seconds. This article documents the complete technical architecture of SENTRICK™ MASTER CORE v2.3: the firmware, the hardware stack, the design decisions, and the specific engineering challenges we solved along the way.

Whether you are building a Med-Tech IoT device, a livestock security system, or a personal safety tracker, the patterns described here — non-blocking GPS handlers, background Firebase writes, and secure HTTP route definitions — apply directly to any production-grade ESP32 project.

System Status — Live Hardware

📡

WiFi

Connected

📷

Camera

Streaming

🛰️

GPS Fix

12 Satellites

🔥

Firebase

Syncing

🌐

HTTP Server

Running

🔄

OTA Updates

Ready


🔧

The Hardware Stack: Three Components, One System

The SENTRICK™ core hardware is deliberately minimal yet powerful. Three primary components form the foundation of the entire system, each chosen for its precision, reliability, and suitability for wearable and field-deployable IoT applications.

Hardware Component Matrix

ESP32-S3-EYE

Main MCU + Camera

Dual-core Xtensa LX7 @ 240 MHz, 8 MB PSRAM, OV2640 camera, WiFi 802.11 b/g/n

SparkFun NEO-M9N GPS

Satellite Positioning

u-blox M9N chip, 25 Hz update rate, 2.5m CEP accuracy, multi-constellation (GPS/GLONASS/Galileo/BeiDou)

Firebase Realtime DB

Cloud Data Layer

REST API over HTTPS, JSON tree at /Sentrick/devices/SENTRICK-BAND-001, sub-100ms latency

The ESP32-S3-EYE development board integrates the camera module directly on the PCB, eliminating the ribbon cable fragility common in other ESP32-CAM variants. Its 8 MB PSRAM is critical for buffering JPEG frames without exhausting the main heap — a common failure point in camera-enabled IoT systems.

The SparkFun NEO-M9N GPS communicates over UART at 9600 baud by default. Its multi-constellation support means the device achieves a GPS fix even in challenging environments — indoors near windows, in dense urban canyons, and across all 190+ countries where SENTRICK™ operates.


The Non-Blocking GPS Architecture: Why It Matters

The most critical architectural decision in the SENTRICK™ v2.3 firmware is the separation of the GPS handler from the Firebase write path. In earlier versions, the gpsHandler function blocked the HTTP server thread while waiting for Firebase to acknowledge the write. This caused the camera stream to stutter and the HTTP server to time out under load.

Anti-Pattern — Blocking GPS Handler (v2.2 and earlier)

// ❌ WRONG: Firebase write blocks the HTTP handler thread
esp_err_t gpsHandler(httpd_req_t *req) {
  // Parse GPS data...
  String lat = getLatitude();
  String lng = getLongitude();

  // This blocks for 200-800ms — kills camera stream
  firebaseClient.setString("/gps/lat", lat);
  firebaseClient.setString("/gps/lng", lng);

  httpd_resp_send(req, "OK", 2);
  return ESP_OK;
}

Correct Pattern — Non-Blocking GPS Handler (v2.3)

// ✅ CORRECT: Handler responds immediately, Firebase write in loop()

// Global state — updated by gpsHandler, consumed by loop()
volatile bool gpsDataPending = false;
String pendingLat = "";
String pendingLng = "";
unsigned long lastGpsWrite = 0;

// HTTP handler — returns immediately, no blocking
esp_err_t gpsHandler(httpd_req_t *req) {
  char buf[256];
  int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
  if (ret <= 0) return ESP_FAIL;
  buf[ret] = '\0';

  // Parse JSON body: {"lat":"29.7604","lng":"-95.3698"}
  StaticJsonDocument<256> doc;
  deserializeJson(doc, buf);
  pendingLat = doc["lat"].as<String>();
  pendingLng = doc["lng"].as<String>();
  gpsDataPending = true;  // Signal loop() to write

  // Respond immediately — no Firebase blocking
  httpd_resp_set_type(req, "application/json");
  httpd_resp_send(req, "{\"status\":\"queued\"}", -1);
  return ESP_OK;
}

// loop() — handles Firebase writes asynchronously
void loop() {
  // Camera stream handler runs here (non-blocking)
  // ...

  // Background GPS → Firebase write (every 2 seconds)
  if (gpsDataPending && millis() - lastGpsWrite > 2000) {
    Firebase.setString(fbdo, "/Sentrick/devices/SENTRICK-BAND-001/gps/lat", pendingLat);
    Firebase.setString(fbdo, "/Sentrick/devices/SENTRICK-BAND-001/gps/lng", pendingLng);
    gpsDataPending = false;
    lastGpsWrite = millis();
  }
}

The result is immediate: the HTTP server responds to the GPS bridge script in under 5ms, the camera stream runs at full frame rate, and Firebase receives clean, throttled updates every two seconds — exactly the behavior required for a production safety device.


📷

The esp_camera Library: SCCB Pin Naming Fix

One of the most common compile errors when working with the esp_camera library on newer ESP-IDF versions is a field name mismatch in the camera configuration struct. The SCCB (Serial Camera Control Bus) pin fields were renamed between library versions, and the compiler error is not always obvious.

Compile Error — Incorrect Field Names

// ❌ Old field names — causes compile error on newer esp_camera versions
config.pin_sscb_sda = SIOD_GPIO_NUM;   // "sscb" → wrong
config.pin_sscb_scl = SIOC_GPIO_NUM;   // "sscb" → wrong

// Error: 'camera_config_t' has no member named 'pin_sscb_sda'

Correct Field Names — SENTRICK™ v2.3

// ✅ Correct field names for esp_camera >= 2.0.x
config.pin_sccb_sda = SIOD_GPIO_NUM;   // "sccb" (double 'c') → correct
config.pin_sccb_scl = SIOC_GPIO_NUM;   // "sccb" (double 'c') → correct

// Full camera config for ESP32-S3-EYE
camera_config_t config;
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer   = LEDC_TIMER_0;
config.pin_d0       = Y2_GPIO_NUM;
config.pin_d1       = Y3_GPIO_NUM;
config.pin_d2       = Y4_GPIO_NUM;
config.pin_d3       = Y5_GPIO_NUM;
config.pin_d4       = Y6_GPIO_NUM;
config.pin_d5       = Y7_GPIO_NUM;
config.pin_d6       = Y8_GPIO_NUM;
config.pin_d7       = Y9_GPIO_NUM;
config.pin_xclk     = XCLK_GPIO_NUM;
config.pin_pclk     = PCLK_GPIO_NUM;
config.pin_vsync    = VSYNC_GPIO_NUM;
config.pin_href     = HREF_GPIO_NUM;
config.pin_sccb_sda = SIOD_GPIO_NUM;   // ← note: sccb not sscb
config.pin_sccb_scl = SIOC_GPIO_NUM;   // ← note: sccb not sscb
config.pin_pwdn     = PWDN_GPIO_NUM;
config.pin_reset    = RESET_GPIO_NUM;
config.xclk_freq_hz = 20000000;
config.pixel_format = PIXFORMAT_JPEG;
config.frame_size   = FRAMESIZE_VGA;
config.jpeg_quality = 12;
config.fb_count     = 2;

This single character difference — sscb vs sccb — has caused hours of debugging for countless ESP32 developers. The SENTRICK™ firmware standardizes on the correct sccb naming throughout.


🔒

Secure HTTP Route Definitions in Arduino ESP32

The ESP-IDF HTTP server (httpd) requires explicit initialization of httpd_uri_t structures. A common mistake is using C99 designated initializer syntax, which can produce unexpected behavior with certain ESP-IDF versions and compilers. The SENTRICK™ firmware uses explicit field-by-field initialization for maximum compatibility and clarity.

SENTRICK™ HTTP Route Registration Pattern

// Explicit struct initialization — safe across all ESP-IDF versions

// GPS data endpoint
httpd_uri_t gpsUri;
gpsUri.uri     = "/gps";
gpsUri.method  = HTTP_POST;
gpsUri.handler = gpsHandler;
gpsUri.user_ctx = NULL;
httpd_register_uri_handler(server, &gpsUri);

// Camera stream endpoint
httpd_uri_t streamUri;
streamUri.uri     = "/stream";
streamUri.method  = HTTP_GET;
streamUri.handler = streamHandler;
streamUri.user_ctx = NULL;
httpd_register_uri_handler(server, &streamUri);

// Status / health check endpoint
httpd_uri_t statusUri;
statusUri.uri     = "/status";
statusUri.method  = HTTP_GET;
statusUri.handler = statusHandler;
statusUri.user_ctx = NULL;
httpd_register_uri_handler(server, &statusUri);

// OTA firmware update endpoint
httpd_uri_t otaUri;
otaUri.uri     = "/ota";
otaUri.method  = HTTP_POST;
otaUri.handler = otaHandler;
otaUri.user_ctx = NULL;
httpd_register_uri_handler(server, &otaUri);

Each route is registered individually with its own httpd_uri_t struct. This pattern makes it trivial to add authentication middleware, rate limiting, or request logging to individual endpoints without touching the others — a critical requirement for a production safety device that may be accessed over public networks.


🌉

The GPS Bridge: Mac → ESP32 → Firebase Data Flow

During development, the SparkFun NEO-M9N GPS module connects to a Mac over USB-serial (appearing as /dev/cu.usbmodem1201). A lightweight Python bridge script reads the NMEA sentences, parses the GPS fix, and forwards the coordinates to the ESP32 HTTP server every two seconds.

Data Flow Architecture

🛰️

NEO-M9N GPS

UART @ 9600 baud

🐍

Python Bridge

pyserial + requests

📡

ESP32-S3

HTTP POST /gps

🔥

Firebase

REST API HTTPS

GPS Bridge Script — Python 3 (gps.py)

#!/usr/bin/env python3
"""
SENTRICK™ GPS Bridge — Mac → ESP32 → Firebase
Reads SparkFun NEO-M9N GPS via USB serial,
forwards coordinates to ESP32 HTTP server every 2 seconds.
"""
import serial
import requests
import time
import json

GPS_PORT  = "/dev/cu.usbmodem1201"   # SparkFun NEO-M9N on Mac
ESP32_IP  = "192.168.1.70"           # ESP32-S3-EYE local IP
ESP32_URL = f"http://{ESP32_IP}/gps"
INTERVAL  = 2.0                       # seconds between updates
TIMEOUT   = 8.0                       # HTTP request timeout

def parse_gga(sentence: str):
    """Parse NMEA GGA sentence → (lat, lng) or None."""
    parts = sentence.split(",")
    if len(parts) < 6 or parts[2] == "" or parts[4] == "":
        return None
    # Convert NMEA ddmm.mmmm → decimal degrees
    raw_lat, lat_dir = parts[2], parts[3]
    raw_lng, lng_dir = parts[4], parts[5]
    lat = int(raw_lat[:2]) + float(raw_lat[2:]) / 60
    lng = int(raw_lng[:3]) + float(raw_lng[3:]) / 60
    if lat_dir == "S": lat = -lat
    if lng_dir == "W": lng = -lng
    return round(lat, 6), round(lng, 6)

def main():
    ser = serial.Serial(GPS_PORT, 9600, timeout=1)
    print(f"[GPS Bridge] Connected to {GPS_PORT}")
    last_send = 0
    while True:
        line = ser.readline().decode("ascii", errors="ignore").strip()
        if not line.startswith("$GPGGA") and not line.startswith("$GNGGA"):
            continue
        result = parse_gga(line)
        if result is None:
            continue
        lat, lng = result
        if time.time() - last_send < INTERVAL:
            continue
        payload = {"lat": str(lat), "lng": str(lng)}
        try:
            r = requests.post(ESP32_URL, json=payload, timeout=TIMEOUT)
            print(f"[GPS] {lat}, {lng} → ESP32: {r.status_code}")
            last_send = time.time()
        except requests.exceptions.RequestException as e:
            print(f"[GPS] Send failed: {e}")

if __name__ == "__main__":
    main()

The bridge script is intentionally stateless and fault-tolerant. If the ESP32 is unreachable (rebooting, OTA update in progress), the script simply logs the failure and retries on the next interval. This design mirrors the resilience requirements of a real-world safety device that must continue operating even when individual subsystems temporarily fail.


🔥

Firebase Realtime Database: Data Structure & Live Tracking

The SENTRICK™ Firebase tree is organized for both per-device historical data and a global live-tracking feed. This dual-path structure allows the mobile app and web dashboard to subscribe to either the device-specific stream or the aggregated live view without additional server logic.

Firebase JSON Tree — /Sentrick

{
  "Sentrick": {
    "devices": {
      "SENTRICK-BAND-001": {
        "gps": {
          "lat": "29.760427",
          "lng": "-95.369804",
          "timestamp": 1747008000000,
          "accuracy": "2.5",
          "satellites": 12
        },
        "status": {
          "online": true,
          "battery": 87,
          "firmware": "v2.3",
          "lastSeen": 1747008000000
        },
        "alerts": {
          "fall_detected": false,
          "geofence_breach": false,
          "sos_triggered": false
        }
      }
    },
    "live": {
      "SENTRICK-BAND-001": {
        "lat": "29.760427",
        "lng": "-95.369804",
        "ts": 1747008000000
      }
    }
  }
}

The /devices/live path is a lightweight mirror of the current GPS position, updated every two seconds. The mobile app subscribes to this path for the real-time map view, while the full device path contains the complete state including battery, firmware version, and alert flags.


🌍

Real-World Applications: From Elderly Safety to Livestock Security

The same hardware and firmware stack that powers the SENTRICK™ development prototype is the foundation for every product vertical in the SENTRICK™ ecosystem. The architecture is designed to scale from a single wearable to a fleet of thousands of devices, all reporting to the same Firebase tree with per-device namespacing.


💡

Key Engineering Lessons from SENTRICK™ v2.3

01

Never block HTTP handlers with network I/O

Firebase writes, MQTT publishes, and any network call must be deferred to the main loop or a background task. HTTP handlers should parse, store, and return immediately.

02

Verify library field names against your exact version

The esp_camera sccb/sscb naming issue is a perfect example of how a single character difference can cause hours of debugging. Always check the library header files directly.

03

Use explicit struct initialization for ESP-IDF types

Designated initializer syntax (.field = value) can behave differently across C/C++ standards and compiler versions. Explicit field-by-field assignment is always safe.

04

Throttle cloud writes to prevent rate limiting

Firebase free tier has write limits. A 2-second throttle on GPS writes reduces Firebase operations by 50x compared to writing on every GPS sentence, while maintaining real-time UX.

05

Design for resilience from day one

Every subsystem (WiFi, GPS, Firebase, camera) should fail independently without crashing the others. The SENTRICK™ firmware uses separate error handling for each subsystem.


🛰️

Experience SENTRICK™ in Action

The technology described in this article powers every SENTRICK™ device — from the elderly safety band to the livestock tracker. Join thousands of early adopters worldwide and be among the first to experience real-time GPS + camera safety.

Tags

#ESP32S3#GPSTracker#IoT#Firebase#MedTechIoT#LivestackSecurity#ElderlySafety#FallDetection#AnimaTechnology#Sentrick#EmbeddedSystems#Arduino#RealTimeGPS#CameraIoT#SafetyDevice
🍪
We use cookies to enhance your experience and analyze site traffic. By continuing, you agree to our Privacy Policy.
AI