Skip to content

Network Interface & Web System

Overview

The network system provides WiFi connectivity in multiple operational modes with a comprehensive web interface for real-time monitoring and configuration. The system implements GPIO-based mode selection, automatic reconnection, security features, and over-the-air (OTA) update capabilities.

WiFi System Architecture

Operational Modes

The system operates in three distinct modes based on GPIO pin states and configuration:

enum OperationalMode {
  CONFIG_AP_MODE,       // First boot configuration
  OPERATIONAL_AP_MODE,  // Permanent AP with full functionality
  CLIENT_MODE           // Connected to ship's WiFi
};

OperationalMode getCurrentMode() {
  if (currentWiFiMode == AWIFI_MODE_CLIENT) {
    return CLIENT_MODE;
  } else if (permanentAPMode == 1) {
    return OPERATIONAL_AP_MODE;
  } else {
    return CONFIG_AP_MODE;
  }
}

GPIO Mode Selection

WiFi mode is determined by GPIO pin states during startup:

void setupWiFi() {
  // GPIO35 = Configuration Mode Override (highest priority)
  pinMode(35, INPUT_PULLUP);
  bool forceConfigMode = (digitalRead(35) == LOW);

  if (forceConfigMode) {
    Serial.println("=== GPIO35 LOW: FORCED CONFIGURATION MODE ===");
    setupAccessPoint();
    setupWiFiConfigServer();
    currentWiFiMode = AWIFI_MODE_AP;
    return;
  }

  // Check for existing configuration
  bool hasClientConfig = LittleFS.exists(WIFI_SSID_FILE);
  String saved_ssid = "";
  String saved_password = "";

  if (hasClientConfig) {
    saved_ssid = readFile(LittleFS, WIFI_SSID_FILE);
    saved_password = readFile(LittleFS, WIFI_PASS_FILE);
    saved_ssid.trim();
    saved_password.trim();
  }

  // GPIO34 = Operational Mode Selection (AP vs Client)
  pinMode(34, INPUT_PULLUP);
  bool requestAPMode = (digitalRead(34) == LOW);

  if (requestAPMode) {
    Serial.println("=== GPIO34 LOW: OPERATIONAL AP MODE ===");
    setupAccessPoint();
    currentWiFiMode = AWIFI_MODE_AP;
    setupServer();  // Full alternator interface
    return;
  }

  // GPIO34 HIGH = Client Mode Requested
  if (connectToWiFi(saved_ssid.c_str(), saved_password.c_str(), 10000)) {
    currentWiFiMode = AWIFI_MODE_CLIENT;
    setupServer();
  } else {
    currentWiFiMode = AWIFI_MODE_CLIENT;  // Keep trying
  }
}

GPIO Configuration: - GPIO35 LOW: Force configuration mode (password recovery) - GPIO34 LOW: Operational AP mode (permanent hotspot) - GPIO34 HIGH: Client mode (connect to ship's WiFi) - No saved config: Automatic configuration mode

Access Point Setup

void setupAccessPoint() {
  WiFi.mode(WIFI_AP);

  bool apStarted = WiFi.softAP(esp32_ap_ssid.c_str(), esp32_ap_password.c_str());

  if (apStarted) {
    Serial.print("AP SSID: ");
    Serial.println(esp32_ap_ssid);
    Serial.print("AP IP address: ");
    Serial.println(WiFi.softAPIP());  // Typically 192.168.4.1

    // Start DNS server for captive portal
    dnsServer.start(DNS_PORT, "*", WiFi.softAPIP());
  }
}

Default AP Configuration: - SSID: "ALTERNATOR_WIFI" (user configurable) - Password: "alternator123" (user configurable) - IP Address: 192.168.4.1 - DHCP Range: 192.168.4.2-192.168.4.100

Client Mode Connection

bool connectToWiFi(const char *ssid, const char *password, unsigned long timeout) {
  WiFi.disconnect(true);
  delay(100);
  WiFi.mode(WIFI_STA);
  WiFi.setAutoReconnect(true);

  if (strlen(password) > 0) {
    WiFi.begin(ssid, password);
  } else {
    WiFi.begin(ssid);  // Open network
  }

  unsigned long startTime = millis();
  int attempts = 0;
  const int maxAttempts = timeout / 500;

  while (WiFi.status() != WL_CONNECTED && attempts < maxAttempts) {
    delay(500);
    esp_task_wdt_reset();  // Feed watchdog during connection
    attempts++;

    if (attempts % 4 == 0) {  // Progress every 2 seconds
      Serial.printf("WiFi Status: %d, attempt %d/%d\n", WiFi.status(), attempts, maxAttempts);
    }
  }

  if (WiFi.status() == WL_CONNECTED) {
    Serial.printf("IP address: %s\n", WiFi.localIP().toString().c_str());
    Serial.printf("Signal strength: %d dBm\n", WiFi.RSSI());

    // mDNS setup for easy access
    if (MDNS.begin("alternator")) {
      MDNS.addService("http", "tcp", 80);
    }
    return true;
  }

  return false;
}

Intelligent Reconnection System

Exponential Backoff Algorithm

struct WiFiReconnection {
  unsigned long lastAttempt = 0;
  int attemptCount = 0;
  unsigned long currentInterval = 2000;      // Start at 2 seconds
  const unsigned long minInterval = 2000;    // 2 seconds minimum
  const unsigned long maxInterval = 300000;  // 5 minutes maximum
  const int maxAttempts = 20;                // Give up after 20 attempts
  bool giveUpMode = false;
  int lastSignalStrength = -999;             // Track signal strength
  const int minSignalThreshold = -80;        // Don't retry aggressively if weak
} wifiRecon;

void checkWiFiConnection() {
  if (currentWiFiMode != AWIFI_MODE_CLIENT) return;

  if (WiFi.status() == WL_CONNECTED) {
    wifiRecon.lastSignalStrength = WiFi.RSSI();
    if (wifiRecon.attemptCount > 0) {
      queueConsoleMessage("WiFi reconnected after " + String(wifiRecon.attemptCount) + " attempts");
    }
    // Reset reconnection state on success
    wifiRecon.attemptCount = 0;
    wifiRecon.currentInterval = wifiRecon.minInterval;
    wifiRecon.giveUpMode = false;
    return;
  }

  // Check give-up mode
  if (wifiRecon.giveUpMode) {
    if (millis() - wifiRecon.lastAttempt < 300000) return;  // 5 minutes
    wifiRecon.giveUpMode = false;
    wifiRecon.attemptCount = 0;
    wifiRecon.currentInterval = wifiRecon.minInterval;
  }

  // Signal strength awareness
  if (wifiRecon.lastSignalStrength < wifiRecon.minSignalThreshold) {
    if (millis() - wifiRecon.lastAttempt < 60000) return;  // 1 minute for weak signal
  } else {
    if (millis() - wifiRecon.lastAttempt < wifiRecon.currentInterval) return;
  }

  wifiRecon.lastAttempt = millis();
  wifiRecon.attemptCount++;

  // Load credentials and attempt connection
  String saved_ssid = readFile(LittleFS, WIFI_SSID_FILE);
  String saved_password = readFile(LittleFS, WIFI_PASS_FILE);
  saved_ssid.trim();
  saved_password.trim();

  bool connected = connectToWiFi(saved_ssid.c_str(), saved_password.c_str(), 10000);

  if (!connected) {
    if (wifiRecon.attemptCount >= wifiRecon.maxAttempts) {
      queueConsoleMessage("WiFi: Max reconnection attempts reached, will retry in 5 minutes");
      wifiRecon.giveUpMode = true;
      return;
    }

    // Exponential backoff with caps
    if (wifiRecon.currentInterval < 32000) {
      wifiRecon.currentInterval *= 2;
    } else if (wifiRecon.currentInterval < 60000) {
      wifiRecon.currentInterval = 60000;
    } else {
      wifiRecon.currentInterval = wifiRecon.maxInterval;
    }
  }
}

Reconnection Strategy: - Progressive delays: 2s, 4s, 8s, 16s, 32s, 60s, 120s, 300s (max) - Signal awareness: Longer delays for weak signal conditions - Give-up mode: 5-minute pause after 20 failed attempts - Automatic recovery: Fresh attempt burst after give-up period

Disconnect Detection

// Simple ping method for disconnect detection
static unsigned long lastPingTime = 0;
static unsigned long lastDisconnectCheck = 0;
static bool alreadyCountedDisconnect = false;

if (millis() - lastDisconnectCheck > 10000) {  // Check every 10 seconds
  lastDisconnectCheck = millis();
  if (millis() - lastPingTime > 25000 && lastPingTime > 0) {
    // WiFi is disconnected
    if (!alreadyCountedDisconnect) {
      wifiDisconnectCount++;
      wifiReconnectsTotal++;
      queueConsoleMessage("WiFi disconnect #" + String(wifiDisconnectCount));
      alreadyCountedDisconnect = true;
    }
  } else if (lastPingTime > 0 && (millis() - lastPingTime < 25000)) {
    alreadyCountedDisconnect = false;  // Reset for next disconnect
  }
}

// Ping handler updates lastPingTime
server.on("/ping", HTTP_GET, [](AsyncWebServerRequest *request) {
  lastPingTime = millis();
  request->send(200, "text/plain", "ok");
});

Web Server Implementation

Server Setup

AsyncWebServer server(80);
AsyncEventSource events("/events");

void setupServer() {
  // Main interface
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
    request->send(LittleFS, "/index.html", "text/html");
  });

  // Settings handler (master endpoint for all configuration)
  server.on("/get", HTTP_GET, [](AsyncWebServerRequest *request) {
    if (!request->hasParam("password") || 
        strcmp(request->getParam("password")->value().c_str(), requiredPassword) != 0) {
      request->send(403, "text/plain", "Forbidden");
      return;
    }

    // Process 80+ different settings parameters
    bool foundParameter = false;
    String inputMessage;

    if (request->hasParam("TemperatureLimitF")) {
      foundParameter = true;
      inputMessage = request->getParam("TemperatureLimitF")->value();
      writeFile(LittleFS, "/TemperatureLimitF.txt", inputMessage.c_str());
      TemperatureLimitF = inputMessage.toInt();
    } 
    // ... 80+ more parameter handlers

    request->send(200, "text/plain", inputMessage);
  });

  // Password management
  server.on("/setPassword", HTTP_POST, [](AsyncWebServerRequest *request) {
    // Validate current password and set new one
  });

  server.on("/checkPassword", HTTP_POST, [](AsyncWebServerRequest *request) {
    // Validate password for access control
  });

  // File serving with proper MIME types
  server.onNotFound([](AsyncWebServerRequest *request) {
    String path = request->url();
    if (LittleFS.exists(path)) {
      String contentType = getContentType(path);
      request->send(LittleFS, path, contentType);
    } else {
      request->redirect("http://" + WiFi.softAPIP().toString());
    }
  });

  // Event source for real-time updates
  events.onConnect([](AsyncEventSourceClient *client) {
    client->send("hello!", NULL, millis(), 10000);
  });

  server.addHandler(&events);
  server.begin();
}

Settings Management System

The system implements a massive settings handler supporting 80+ parameters:

// Example parameter handling in /get endpoint
if (request->hasParam("BulkVoltage")) {
  foundParameter = true;
  inputMessage = request->getParam("BulkVoltage")->value();
  writeFile(LittleFS, "/BulkVoltage.txt", inputMessage.c_str());
  BulkVoltage = inputMessage.toFloat();
} else if (request->hasParam("TargetAmps")) {
  foundParameter = true;
  inputMessage = request->getParam("TargetAmps")->value();
  writeFile(LittleFS, "/TargetAmps.txt", inputMessage.c_str());
  TargetAmps = inputMessage.toInt();
}
// ... continues for all parameters

Settings Architecture: - Single endpoint: /get handles all parameter updates - Password protection: All changes require authentication - Immediate persistence: Settings saved to LittleFS files immediately - Live updates: Changes take effect without restart - Parameter validation: Type conversion and bounds checking

Real-Time Data Streaming

SendWifiData() Architecture

The core data transmission function implements a priority-based streaming system:

void SendWifiData() {
  static unsigned long prev_millis5 = 0;            // CSVData timing
  static unsigned long lastConsoleMessageTime = 0;  // Console timing
  static unsigned long lastpayload2send = 0;        // CSVData2 timing
  static unsigned long lastpayload3send = 0;        // CSVData3 timing
  static unsigned long lastTimestampSend = 0;       // TimestampData timing
  static unsigned long lastEventSourceSend = 0;     // Global cooldown

  const unsigned long EVENTSOURCE_COOLDOWN = 8;     // 8ms between sends
  const unsigned long CONSOLE_MESSAGE_INTERVAL = 1000;

  unsigned long now = millis();

  // Global cooldown check
  bool canSendNow = (now - lastEventSourceSend >= EVENTSOURCE_COOLDOWN);
  if (!canSendNow) return;

  bool sentSomething = false;

  // PRIORITY 1: CSVData (real-time sensor data)
  if (!sentSomething && now - prev_millis5 >= webgaugesinterval && events.count() > 0) {
    static char payload1[420];
    snprintf(payload1, sizeof(payload1),
             "%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d",
             SafeInt(AlternatorTemperatureF),  // 0
             SafeInt(dutyCycle),               // 1
             SafeInt(BatteryV, 100),           // 2
             // ... 35 total values
    );
    events.send(payload1, "CSVData");
    prev_millis5 = now;
    lastEventSourceSend = now;
    sentSomething = true;
  }

  // PRIORITY 2: Console messages
  if (!sentSomething && now - lastConsoleMessageTime >= CONSOLE_MESSAGE_INTERVAL && consoleCount > 0) {
    int messagesToSend = min(2, consoleCount);
    for (int i = 0; i < messagesToSend; i++) {
      if (consoleCount > 0) {
        events.send(consoleQueue[consoleTail].message, "console");
        consoleTail = (consoleTail + 1) % CONSOLE_QUEUE_SIZE;
        consoleCount--;
      }
    }
    lastConsoleMessageTime = now;
    lastEventSourceSend = now;
    sentSomething = true;
  }

  // PRIORITY 3: CSVData2 (status data - every 2 seconds)
  if (!sentSomething && now - lastpayload2send >= 2000) {
    static char payload2[700];
    // 85 status values including SOC, energy, alarms, etc.
    events.send(payload2, "CSVData2");
    lastpayload2send = now;
    lastEventSourceSend = now;
    sentSomething = true;
  }

  // PRIORITY 4: CSVData3 (settings data - every 2 seconds)  
  if (!sentSomething && now - lastpayload3send >= 2000) {
    static char payload3[1000];
    // 149 configuration values including all settings
    events.send(payload3, "CSVData3");
    lastpayload3send = now;
    lastEventSourceSend = now;
    sentSomething = true;
  }

  // PRIORITY 5: TimestampData (staleness data - every 3 seconds)
  if (!sentSomething && now - lastTimestampSend >= 3000) {
    static char timestampPayload[400];
    // Data freshness timestamps for all 17 sensor channels
    events.send(timestampPayload, "TimestampData");
    lastTimestampSend = now;
    lastEventSourceSend = now;
    sentSomething = true;
  }
}

Data Payload Structure

CSVData (Priority 1 - Real-time)

Update Rate: User configurable (50ms default) Size: 35 values Content: Core sensor readings - Temperature, duty cycle, voltages, currents - RPM, timing data, WiFi strength - Plot control parameters

CSVData2 (Priority 3 - Status)

Update Rate: 2 seconds Size: 85 values
Content: Status and monitoring data - SOC, energy totals, runtime statistics - Temperature maximums, alarm states - Weather data, system health metrics

CSVData3 (Priority 4 - Settings)

Update Rate: 2 seconds Size: 149 values Content: All configuration parameters - Basic settings (voltage, current targets) - Advanced settings (PID, learning parameters) - RPM tables, learning diagnostics

TimestampData (Priority 5 - Staleness)

Update Rate: 3 seconds Size: 17 values Content: Data freshness indicators - Time since last update for each sensor - Stale data detection for web interface - Sensor failure indication

Data Scaling and Encoding

The system uses consistent scaling factors for transmission:

int SafeInt(float f, int scale = 1) {
  return isnan(f) || isinf(f) ? -1 : (int)(f * scale);
}

// Common scaling patterns:
SafeInt(BatteryV, 100),           // Voltage × 100 (13.75V → 1375)
SafeInt(MeasuredAmps, 100),       // Current × 100 (42.5A → 4250)  
SafeInt(iiout, 10),               // Field current × 10 (2.35A → 235)
SafeInt(DynamicShuntGainFactor, 1000),  // Gain × 1000 (1.025 → 1025)
SafeInt(rpmCurrentTable[0], 100), // Learning table × 100 (45.5A → 4550)

Scaling Benefits: - Integer transmission: Avoids floating point parsing in JavaScript - Precision preservation: Maintains 2-3 decimal places - Error indication: -1 value indicates invalid/NaN data - Bandwidth efficiency: Smaller payload size than JSON

Security System

Password Management

// Password storage and validation
char requiredPassword[32] = "admin";  // Default password
char storedPasswordHash[65] = {0};    // SHA-256 hash storage

void loadPasswordHash() {
  // Load plaintext password for auth
  if (LittleFS.exists("/password.txt")) {
    File plainFile = LittleFS.open("/password.txt", "r");
    String pwdStr = plainFile.readStringUntil('\n');
    pwdStr.trim();
    strncpy(requiredPassword, pwdStr.c_str(), sizeof(requiredPassword) - 1);
    plainFile.close();
  }

  // Load hash for future use
  if (LittleFS.exists("/password.hash")) {
    File file = LittleFS.open("/password.hash", "r");
    size_t len = file.readBytesUntil('\n', storedPasswordHash, sizeof(storedPasswordHash) - 1);
    storedPasswordHash[len] = '\0';
    file.close();
  } else {
    // Create default hash
    strncpy(requiredPassword, "admin", sizeof(requiredPassword) - 1);
    sha256("admin", storedPasswordHash);
  }
}

bool validatePassword(const char *password) {
  if (!password) return false;
  char hash[65] = {0};
  sha256(password, hash);
  return (strcmp(hash, storedPasswordHash) == 0);
}

SHA-256 Implementation

void sha256(const char *input, char *outputBuffer) {
  byte shaResult[32];
  mbedtls_md_context_t ctx;
  const mbedtls_md_info_t *info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);

  mbedtls_md_init(&ctx);
  mbedtls_md_setup(&ctx, info, 0);
  mbedtls_md_starts(&ctx);
  mbedtls_md_update(&ctx, (const unsigned char *)input, strlen(input));
  mbedtls_md_finish(&ctx, shaResult);
  mbedtls_md_free(&ctx);

  for (int i = 0; i < 32; ++i) {
    sprintf(outputBuffer + (i * 2), "%02x", shaResult[i]);
  }
}

Access Control

All configuration changes require password validation:

server.on("/get", HTTP_GET, [](AsyncWebServerRequest *request) {
  if (!request->hasParam("password") || 
      strcmp(request->getParam("password")->value().c_str(), requiredPassword) != 0) {
    request->send(403, "text/plain", "Forbidden");
    return;
  }
  // Process setting changes...
});

WiFi Configuration System

Configuration Interface

void setupWiFiConfigServer() {
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
    request->send_P(200, "text/html", WIFI_CONFIG_HTML);
  });

  server.on("/wifi", HTTP_POST, [](AsyncWebServerRequest *request) {
    String ssid = "";
    String password = "";
    String ap_password = "";
    String hotspot_ssid = "";

    // Extract parameters
    if (request->hasParam("ssid", true)) {
      ssid = request->getParam("ssid", true)->value();
      ssid.trim();
    }
    if (request->hasParam("password", true)) {
      password = request->getParam("password", true)->value();
      password.trim();
    }
    if (request->hasParam("ap_password", true)) {
      ap_password = request->getParam("ap_password", true)->value();
      ap_password.trim();
    }
    if (request->hasParam("hotspot_ssid", true)) {
      hotspot_ssid = request->getParam("hotspot_ssid", true)->value();
      hotspot_ssid.trim();
    }

    // Apply defaults
    if (ap_password.length() == 0) {
      ap_password = "alternator123";
    } else if (ap_password.length() < 8) {
      ap_password = "alternator123";  // Security requirement
    }

    // Save configuration
    writeFile(LittleFS, AP_PASSWORD_FILE, ap_password.c_str());
    esp32_ap_password = ap_password;

    if (hotspot_ssid.length() > 0) {
      writeFile(LittleFS, AP_SSID_FILE, hotspot_ssid.c_str());
      esp32_ap_ssid = hotspot_ssid;
    }

    writeFile(LittleFS, "/ssid.txt", ssid.c_str());
    writeFile(LittleFS, "/pass.txt", password.c_str());

    request->send(200, "text/plain", "Configuration saved! Device will restart in 3 seconds.");
    delay(3000);
    ESP.restart();
  });

  server.begin();
}

Captive Portal Implementation

void setupAccessPoint() {
  WiFi.mode(WIFI_AP);
  WiFi.softAP(esp32_ap_ssid.c_str(), esp32_ap_password.c_str());

  // DNS server redirects all requests to AP IP
  dnsServer.start(DNS_PORT, "*", WiFi.softAPIP());
}

void dnsHandleRequest() {
  if (currentWiFiMode == AWIFI_MODE_AP) {
    dnsServer.processNextRequest();
  }
}

// 404 handler redirects to captive portal
server.onNotFound([](AsyncWebServerRequest *request) {
  if (LittleFS.exists(request->url())) {
    // Serve file if it exists
    request->send(LittleFS, request->url(), getContentType(request->url()));
  } else {
    // Redirect to captive portal
    request->redirect("http://" + WiFi.softAPIP().toString());
  }
});

Over-The-Air (OTA) Updates

OTA System Architecture

const char *OTA_SERVER_URL = "https://ota.xengineering.net";
const char *FIRMWARE_VERSION = "1.9.0";

// Server certificate for HTTPS validation
const char *server_root_ca = "-----BEGIN CERTIFICATE-----\n"
  "MIIFBTCCAu2gAwIBAgIQS6hSk/eaL6JzBkuoBI110DANBgkqhkiG9w0BAQsFADBP\n"
  // ... certificate content
  "-----END CERTIFICATE-----\n";

// RSA public key for firmware signature verification
const char *OTA_PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----\n"
  "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp2sRgMjD4wazKHo6Rk3g\n"
  // ... public key content
  "-----END PUBLIC KEY-----\n";

Update Process

void checkForOTAUpdate() {
  HTTPClient http;
  WiFiClientSecure client;
  client.setCACert(server_root_ca);

  String url = String(OTA_SERVER_URL) + "/api/firmware/check.php";
  http.begin(client, url);

  // Add device identification headers
  http.addHeader("Device-ID", getDeviceId());
  http.addHeader("Current-Version", FIRMWARE_VERSION);
  http.addHeader("Hardware-Version", "ESP32-WROOM-32");

  int httpCode = http.GET();

  if (httpCode == 200) {
    String response = http.getString();
    UpdateInfo updateInfo = parseUpdateResponse(response);

    if (updateInfo.hasUpdate) {
      performOTAUpdate(updateInfo);
    }
  }

  http.end();
}

Streaming TAR Extraction

The OTA system implements streaming extraction of TAR packages:

struct StreamingExtractor {
  bool inTarHeader;
  uint8_t tarHeader[512];
  size_t tarHeaderPos;
  String currentFileName;
  size_t currentFileSize;
  bool isCurrentFileFirmware;
  esp_ota_handle_t otaHandle;
  const esp_partition_t* otaPartition;
  bool otaStarted;
  mbedtls_md_context_t hashCtx;  // For signature verification
};

bool processDataChunk(StreamingExtractor* extractor, uint8_t* data, size_t dataSize) {
  size_t processed = 0;

  while (processed < dataSize) {
    if (extractor->inTarHeader) {
      // Read TAR header
      size_t headerRemaining = 512 - extractor->tarHeaderPos;
      size_t toCopy = min(headerRemaining, dataSize - processed);

      memcpy(extractor->tarHeader + extractor->tarHeaderPos, data + processed, toCopy);
      extractor->tarHeaderPos += toCopy;
      processed += toCopy;

      if (extractor->tarHeaderPos >= 512) {
        if (!parseTarHeader(extractor)) return false;
        extractor->inTarHeader = false;
      }
    } else {
      // Read file data
      size_t fileRemaining = extractor->currentFileSize - extractor->currentFilePos;
      size_t toWrite = min(fileRemaining, dataSize - processed);

      if (extractor->isCurrentFileFirmware && extractor->otaStarted) {
        esp_err_t err = esp_ota_write(extractor->otaHandle, data + processed, toWrite);
        if (err != ESP_OK) return false;
      }

      extractor->currentFilePos += toWrite;
      processed += toWrite;
    }
  }

  return true;
}

Signature Verification

bool verifyPackageSignature(uint8_t* packageData, size_t packageSize, const String& signatureBase64) {
  mbedtls_pk_context pk;
  uint8_t signature[520];
  size_t sigLength;

  // Decode signature
  if (!base64Decode(signatureBase64, signature, sizeof(signature), &sigLength)) {
    return false;
  }

  // Hash package
  uint8_t hash[32];
  mbedtls_md_context_t ctx;
  mbedtls_md_init(&ctx);
  const mbedtls_md_info_t* info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);

  mbedtls_md_setup(&ctx, info, 0);
  mbedtls_md_starts(&ctx);
  mbedtls_md_update(&ctx, packageData, packageSize);
  mbedtls_md_finish(&ctx, hash);
  mbedtls_md_free(&ctx);

  // Verify signature with RSA public key
  mbedtls_pk_init(&pk);
  int ret = mbedtls_pk_parse_public_key(&pk, (const unsigned char*)OTA_PUBLIC_KEY, strlen(OTA_PUBLIC_KEY) + 1);
  if (ret != 0) {
    mbedtls_pk_free(&pk);
    return false;
  }

  ret = mbedtls_pk_verify(&pk, MBEDTLS_MD_SHA256, hash, 32, signature, sigLength);
  mbedtls_pk_free(&pk);

  return (ret == 0);
}

Console Message System

Message Queue Implementation

#define CONSOLE_QUEUE_SIZE 10
struct ConsoleMessage {
  char message[128];
  unsigned long timestamp;
};

ConsoleMessage consoleQueue[CONSOLE_QUEUE_SIZE];
volatile int consoleHead = 0;
volatile int consoleTail = 0;
volatile int consoleCount = 0;

void queueConsoleMessage(String message) {
  // Prevent stack overflow
  if (message.length() > 120) {
    message = message.substring(0, 120) + "...";
  }

  // Thread-safe circular buffer
  int nextHead = (consoleHead + 1) % CONSOLE_QUEUE_SIZE;
  int localTail = consoleTail;

  if (nextHead != localTail) {  // Not full
    strncpy(consoleQueue[consoleHead].message, message.c_str(), 127);
    consoleQueue[consoleHead].message[127] = '\0';
    consoleQueue[consoleHead].timestamp = millis();
    consoleHead = nextHead;

    int localCount = consoleCount;
    if (localCount < CONSOLE_QUEUE_SIZE) {
      consoleCount = localCount + 1;
    }
  }
  // If full, oldest message is overwritten
}

Console Features: - Fixed-size buffer: Prevents memory exhaustion - Thread-safe: Atomic operations for interrupt safety - Overflow handling: Oldest messages discarded when full - Timestamp tracking: Message age for debugging - Length limiting: Prevents buffer overruns

Performance Characteristics

Network Bandwidth Usage

Data Stream Update Rate Payload Size Bandwidth
CSVData 50ms (configurable) 420 bytes ~8.4 KB/s
Console 1 second ~50 bytes/msg ~0.1 KB/s
CSVData2 2 seconds 700 bytes ~0.35 KB/s
CSVData3 2 seconds 1000 bytes ~0.5 KB/s
TimestampData 3 seconds 400 bytes ~0.13 KB/s
Total Combined Variable ~9.5 KB/s

Memory Usage

Component RAM Usage Flash Usage
WiFi stack ~15 KB ~200 KB
Web server ~8 KB ~50 KB
TLS/crypto ~20 KB ~150 KB
Console queue 1.3 KB -
Data buffers 2.5 KB -
Total ~47 KB ~400 KB

Client Connection Limits

  • Concurrent connections: 5-10 tested
  • EventSource streams: Limited by ESP32 memory
  • HTTP requests: No artificial limits
  • WebSocket: Not implemented (EventSource preferred)

Configuration Parameters

Network Settings

Parameter Default Description
esp32_ap_ssid "ALTERNATOR_WIFI" Access point SSID
esp32_ap_password "alternator123" Access point password
webgaugesinterval 50 Real-time data update rate (ms)
plotTimeWindow 120 Plot time window (seconds)
EVENTSOURCE_COOLDOWN 8 Minimum time between sends (ms)

WiFi Reconnection

Parameter Default Description
WIFI_TIMEOUT 20000 Connection timeout (ms)
minInterval 2000 Minimum retry interval (ms)
maxInterval 300000 Maximum retry interval (ms)
maxAttempts 20 Attempts before give-up mode
minSignalThreshold -80 Signal strength threshold (dBm)

Security Settings

Parameter Default Description
requiredPassword "admin" Default admin password
Password length 8+ chars Minimum for AP password
Hash algorithm SHA-256 Password hashing method
Certificate validation Enabled HTTPS certificate checking

File System Integration

LittleFS Web Files

server.onNotFound([](AsyncWebServerRequest *request) {
  String path = request->url();

  if (LittleFS.exists(path)) {
    String contentType = "text/html";
    if (path.endsWith(".css")) contentType = "text/css";
    else if (path.endsWith(".js")) contentType = "application/javascript";
    else if (path.endsWith(".json")) contentType = "application/json";
    else if (path.endsWith(".png")) contentType = "image/png";

    request->send(LittleFS, path, contentType);
  } else {
    request->redirect("http://" + WiFi.softAPIP().toString());
  }
});

Settings Persistence

All network configuration is stored in LittleFS:

/ssid.txt          - Client mode WiFi SSID
/pass.txt          - Client mode WiFi password  
/apssid.txt        - AP mode SSID override
/appass.txt        - AP mode password
/password.txt      - Web interface password (plaintext)
/password.hash     - Web interface password (SHA-256)
/wifimode.txt      - Permanent AP mode flag

Troubleshooting Guide

Common Network Issues

No WiFi Connection

Check:
1. GPIO pin states during boot
2. Saved credentials in /ssid.txt and /pass.txt
3. Signal strength and router compatibility
4. DHCP pool availability on router
5. MAC address filtering on router

Web Interface Not Loading

Check:
1. Correct IP address (check serial output)
2. mDNS resolution (try alternator.local)
3. Browser cache and cookies
4. Firewall blocking connections
5. LittleFS filesystem integrity

Real-time Data Not Updating

Check:
1. EventSource connection established
2. webgaugesinterval setting
3. JavaScript console errors
4. Network latency/packet loss
5. Multiple browser tabs competing

OTA Update Failures

Check:
1. Internet connectivity
2. Server certificate validation
3. Available flash space
4. Stable power supply during update
5. Firmware signature verification

Diagnostic Tools

Network Status Monitoring

  • WiFi signal strength display
  • Connection attempt counting
  • Reconnection statistics
  • Ping response tracking

Performance Monitoring

  • Loop time tracking
  • Memory usage monitoring
  • EventSource client counting
  • Console message queue status

Security Monitoring

  • Failed password attempts
  • Certificate validation results
  • Signature verification status
  • Access control violations

The network system provides robust WiFi connectivity with intelligent reconnection, comprehensive web interface, and secure OTA update capabilities, supporting both standalone AP operation and integration with existing ship networks.