Hackster is hosting Hackster Holidays, Ep. 6: Livestream & Giveaway Drawing. Watch previous episodes or stream live on Monday!Stream Hackster Holidays, Ep. 6 on Monday!
Isaiah O.
Published

Project Postman

A package monitoring solution to avoid porch pirates and misplaced packages.

IntermediateWork in progress137
Project Postman

Things used in this project

Story

Read more

Code

ESP32-Postman.ino

Arduino
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <ElegantOTA.h>
#include <WebSerial.h>
#include <LittleFS.h>
#include <SoftwareSerial.h>
#include <Preferences.h>
#include <Seeed_Arduino_SSCMA.h>
#include <ArduinoJson.h>
#include "time.h"
#include "FS.h"
#include "SD.h"
#include "SPI.h"
#include "esp_mac.h"
#include <esp_task_wdt.h>

#define FIRMWAREVERSION "EO 1.0"  // E-ESP32 O-open source 1.0 Version
#define SD_CS_PIN 21              // XIAO ESP32-S3 CS pin
#define host "ProjectPostmanESP"  // Hostname for the netowrk.
#define TASK_STACK_SIZE 8192

const char *AP_ssid = "XIAO_ESP32S3";
const char *AP_password = "123456789";
// String ssid = "Xtream_2.4G_CC93";
// String password = "zwtt3xbe";
String ssid = "Thomas";
String password = "Qcpeds*7";
const char *http_username = "admin";
const char *http_password = "admin";

struct deviceInfo {
  String deviceModel;
  uint32_t chipID;
  uint8_t chipCores;
  String MAC_Address;
  String firmware;
  String ssid;
  String wifi_password;
  int32_t signalstrength;
  String hostname;
  IPAddress IP;
  bool wificonnected;
  String uptime;
  long gmtoffset;
  int daylightoffset;
  unsigned long interval;
  bool setup;
};

struct user {
  String username;
  String password;
  String email;
  String phonenumber;
  char pin[6];
  bool admin;
};

deviceInfo esp32Info = {
  "",         // deviceModel
  0,          // chipID
  0,          // chipCores
  "",         // MAC_Address
  "",         // firmware
  "",         // ssid
  "",         // wifi_password
  0,          // signalstrength
  "",         // hostname
  "",         // IP
  false,      // wificonnected
  "",         // uptime
  -3600 * 5,  // gmtoffset
  0,          // daylightoffset
  30000,      // interval
  false       // setup
};

user userid[6];
AsyncWebServer server(80);       // Create AsyncWebServer object on port 80
SoftwareSerial espSerial(4, 3);  // (D2 = 3) RX, (D3 = 4) TX pins for ESP32
Preferences device;
Preferences userdata;
SSCMA AI;

String requestBody = "";
String lastImage = "";
bool streaming = false;  // Flag to manage streaming state
bool lock = true;        // Flag to manage lock state
unsigned long previousMillis = 0;

void setTime(const char *tz);
void notFound(AsyncWebServerRequest *request);
void rebootESP(String message);
void listDirContents(File dir, String path, String &returnText, bool ishtml);
String listFiles(bool ishtml);
String humanReadableSize(const size_t bytes);
String getContentType(String filename);
String getLocalTimeStr();
bool initSDCard();
void writeFile(fs::FS &fs, const char *path, const char *message);
void appendFile(fs::FS &fs, const char *path, const char *message);
void recordGrove();
void alert(const char *message);
void logSD(const char *message, bool serial = true);
int year();
int month();
void createDirectories(const char *path);
String getInterfaceMacAddress(esp_mac_type_t interface);
bool setupWiFi(const char *newSSID, const char *newPassword);
String getUptime();
String listRecordings();
void listDirContentsJson(File dir, String path, JsonArray &filesArray);
void saveUserData(int index);
void loadUserData(int index);
void saveDeviceInfoToPreferences(const deviceInfo &info);
void getDeviceInfoFromPreferences(deviceInfo &info);
void clearAllPreferences();

void formatLittleFS() {
  Serial.println("Formatting LittleFS...");
  if (LittleFS.format()) {
    Serial.println("LittleFS formatted successfully.");
  } else {
    Serial.println("Failed to format LittleFS.");
  }
}

void notFound(AsyncWebServerRequest *request) {
  request->send(404, "text/plain", "Not found");
}

void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {
  if (!index) {
    Serial.printf("BodyStart: %u B\n", total);
  }
  for (size_t i = 0; i < len; i++) {
    Serial.write(data[i]);
  }
  if (index + len == total) {
    Serial.printf("BodyEnd: %u B\n", total);
  }
}

void setup() {
  Serial.begin(115200);                     // Initialize the Arduino serial port
  espSerial.begin(115200);                  // Initialize the ESP8266 serial port
  getDeviceInfoFromPreferences(esp32Info);  // Retrieve device info from Preferences
  for (int i = 0; i < 6; i++) {
    loadUserData(i);
  }

  if (!LittleFS.begin()) {
    Serial.println("An Error has occurred while mounting LittleFS");
    formatLittleFS();  // Format LittleFS if mounting fails
    if (!LittleFS.begin()) {
      Serial.println("Failed to mount LittleFS after formatting");
      return;
    }
  }
  if (!initSDCard()) {
    Serial.println("An Error has occurred while initiating the SD Card");
    return;
  }
  if (!AI.begin()) {
    Serial.println("Grove Vision AI not initiated");
  }

  esp32Info.chipID = (uint32_t)(ESP.getEfuseMac() >> 32);
  esp32Info.deviceModel = String(ESP.getChipModel()) + " Rev " + String(ESP.getChipRevision());
  esp32Info.chipCores = ESP.getChipCores();
  esp32Info.firmware = FIRMWAREVERSION;
  esp32Info.wificonnected = setupWiFi(ssid.c_str(), password.c_str());

  if (!esp32Info.setup) {
    Serial.println("Device not set up. Starting setup...");
    // Setup file structure
    createDirectories("/alerts");
    createDirectories("/packages");
    createDirectories("/recordings");
    createDirectories("/device");
    createDirectories("/device/logs");
    createDirectories("/device/users");
    createDirectories("/device/config");
    createDirectories("/device/firmware");

    server.on("/finsh", HTTP_GET, [](AsyncWebServerRequest *request) {
      esp32Info.setup = true;
      saveDeviceInfoToPreferences(esp32Info);
      saveUserData(0);
      request->send(200, "text/plain", "Setup Complete");
      Serial.println("Device setup complete. Rebooting ESP32...");
      delay(5000);
      rebootESP("Device setup complete");
    });
  } else {
    Serial.println("Device already set up. Starting main program...");
    for (int i = 0; i < 6; i++)
      loadUserData(i);
  }

  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
    if (!esp32Info.setup) {
      request->send(LittleFS, "/setup.html", "text/html");
    } else {
      request->send(LittleFS, "/index.html", "text/html");
    }
  });

  server.on("/style.css", HTTP_GET, [](AsyncWebServerRequest *request) {
    request->send(LittleFS, "/style.css", "text/css");
  });
  server.on("/script.js", HTTP_GET, [](AsyncWebServerRequest *request) {
    request->send(LittleFS, "/script.js", "application/javascript");
  });
  server.on("/favicon.ico", HTTP_GET, [](AsyncWebServerRequest *request) {
    request->send(LittleFS, "/favicon.ico", "application/javascript");
  });

  server.on("/stream", HTTP_GET, [](AsyncWebServerRequest *request) {
    if (lastImage.length() > 0) {
      String response = lastImage;
      request->send(200, "text/plain", response);
    } else {
      request->send(200, "text/plain", "data:,");  // Send an empty data URL to avoid errors
    }
  });

  server.on("/startStream", HTTP_GET, [](AsyncWebServerRequest *request) {
    streaming = true;
    request->send(200, "text/plain", "Stream started");
  });

  server.on("/stopStream", HTTP_GET, [](AsyncWebServerRequest *request) {
    streaming = false;
    request->send(200, "text/plain", "Stream stopped");
  });

  server.on("/lock", HTTP_GET, [](AsyncWebServerRequest *request) {
    lock = true;
    alert("Lock");
    request->send(200, "text/plain", "Locked");
  });

  server.on("/unlock", HTTP_GET, [](AsyncWebServerRequest *request) {
    lock = false;
    alert("Unlocked");
    request->send(200, "text/plain", "Unlocked");
  });

  server.on("/alerts", HTTP_GET, [](AsyncWebServerRequest *request) {
    char path[64];
    snprintf(path, sizeof(path), "/alerts/%04d-%02d.txt", year(), month());
    File file = SD.open(path, FILE_READ);
    if (file) {
      String alertContent = file.readString();
      request->send(200, "text/plain", alertContent);
      file.close();
    } else {
      request->send(500, "text/plain", "Failed to read file");
    }
  });

  server.on("/list-recordings", HTTP_GET, [](AsyncWebServerRequest *request) {
    String files = listRecordings();
    request->send(200, "application/json", files);
  });

  server.on("/play-recording", HTTP_GET, [](AsyncWebServerRequest *request) {
    if (request->hasParam("file")) {
      String filename = request->getParam("file")->value();
      File file = SD.open("/recordings/" + filename);
      if (file) {
        AsyncWebServerResponse *response = request->beginChunkedResponse("text/plain",
                                                                         [file](uint8_t *buffer, size_t maxLen, size_t index) mutable -> size_t {
                                                                           if (file.available()) {
                                                                             size_t bytesRead = file.read(buffer, maxLen);
                                                                             return bytesRead;
                                                                           } else {
                                                                             file.close();
                                                                             return 0;  // No more data to send
                                                                           }
                                                                         });
        request->send(response);
      } else {
        request->send(404, "text/plain", "File not found");
      }
    } else {
      request->send(400, "text/plain", "Bad Request");
    }
  });

  server.on("/download", HTTP_GET, [](AsyncWebServerRequest *request) {
    if (request->hasParam("file")) {
      String filePath = request->getParam("file")->value();
      if (SD.exists(filePath)) {
        String contentType = getContentType(filePath);
        request->send(SD, filePath.c_str(), contentType, true);
      } else {
        request->send(404, "text/plain", "File not found");
      }
    } else {
      request->send(400, "text/plain", "Filename not specified");
    }
  });

  server.on("/delete", HTTP_DELETE, [](AsyncWebServerRequest *request) {
    if (request->hasParam("file")) {
      String filePath = request->getParam("file")->value();
      if (SD.remove(filePath.c_str())) {
        request->send(200, "text/plain", "File deleted successfully");
      } else {
        request->send(500, "text/plain", "Failed to delete file");
      }
    } else {
      request->send(400, "text/plain", "File not specified");
    }
  });

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

  server.on("/reboot", HTTP_GET, [](AsyncWebServerRequest *request) {
    request->send(200, "text/plain", "Rebooting ESP32...");
    delay(100);
    rebootESP("Requested from the Async Web Server");
  });

  server.on("/scan-networks", HTTP_GET, [](AsyncWebServerRequest *request) {
    int numNetworks = WiFi.scanNetworks();
    String response = "[";
    for (int i = 0; i < numNetworks; i++) {
      if (i > 0) response += ",";
      response += "{\"ssid\":\"" + WiFi.SSID(i) + "\",\"signal_strength\":" + String(WiFi.RSSI(i)) + "}";
    }
    response += "]";
    request->send(200, "application/json", response);
  });

  server.on("/connect", HTTP_GET, [](AsyncWebServerRequest *request) {
    if (request->hasParam("ssid") && request->hasParam("password")) {
      String tempssid = request->getParam("ssid")->value();
      String temppassword = request->getParam("password")->value();
      Serial.println("Connecting to SSID: " + tempssid);

      if (setupWiFi(tempssid.c_str(), temppassword.c_str())) {  // Attempt to connect to the new Wi-Fi network
        esp32Info.wificonnected = true;
        request->send(200, "text/plain", "Connected to new network");
      } else {  // Failed to connect, try reconnecting to the previous network or set up an access point
        if (!setupWiFi(esp32Info.ssid.c_str(), esp32Info.wifi_password.c_str())) {
          esp32Info.wificonnected = false;
          request->send(500, "text/plain", "Failed to connect to new network and access point set up.");
        } else {
          request->send(200, "text/plain", "Failed to connect to new network, reconnected to previous network.");
        }
      }
    } else {
      request->send(400, "text/plain", "Missing SSID or password");
    }
  });

  server.on("/set-timezone", HTTP_GET, [](AsyncWebServerRequest *request) {
    if (request->hasParam("timezone")) {
      String timezone = request->getParam("timezone")->value();
      Serial.println(timezone);
      setTime(timezone.c_str());
    }
    if (request->hasParam("gmtoffset") && request->hasParam("daylightoffset")) {
      esp32Info.gmtoffset = request->getParam("gmtoffset")->value().toInt();
      esp32Info.gmtoffset = esp32Info.gmtoffset * 3600;
      Serial.println(esp32Info.gmtoffset);
      esp32Info.daylightoffset = request->getParam("daylightoffset")->value().toInt();
      Serial.println(esp32Info.daylightoffset);
      configTime(esp32Info.gmtoffset, esp32Info.daylightoffset, "pool.ntp.org");
    }

    time_t now;
    struct tm timeinfo;
    time(&now);
    localtime_r(&now, &timeinfo);

    char timeString[64];
    strftime(timeString, sizeof(timeString), "%Y-%m-%d %H:%M:%S", &timeinfo);
    Serial.println(String(timeString));
    request->send(200, "text/plain", String(timeString));
  });

  server.on("/logs", HTTP_GET, [](AsyncWebServerRequest *request) {
    String logs = listFiles(true);  // Pass true to get HTML-formatted output
    request->send(200, "text/html", logs);
  });

  server.on("/manage-users", HTTP_GET, [](AsyncWebServerRequest *request) {
    DynamicJsonDocument doc(1024);
    JsonArray users = doc.createNestedArray("users");

    for (int i = 0; i < 6; i++) {
      JsonObject userObj = users.createNestedObject();
      userObj["username"] = userid[i].username;
      userObj["password"] = userid[i].password;
      userObj["email"] = userid[i].email;
      userObj["phonenumber"] = userid[i].phonenumber;
      userObj["admin"] = userid[i].admin;
      userObj["pin"] = String(userid[i].pin, 6);
    }

    String response;
    serializeJson(doc, response);
    request->send(200, "application/json", response);
  });

  server.on("/add-user", HTTP_POST, [](AsyncWebServerRequest *request) {
    String name, mail, word, phone, pinStr;
    bool admin;
    if (request->hasParam("username", true)) {
      name = request->getParam("username", true)->value();
    }
    if (request->hasParam("password", true)) {
      word = request->getParam("password", true)->value();
    }
    if (request->hasParam("email", true)) {
      mail = request->getParam("email", true)->value();
    }
    if (request->hasParam("phonenumber", true)) {
      phone = request->getParam("phonenumber", true)->value();
    }
    if (request->hasParam("pin", true)) {
      pinStr = request->getParam("pin", true)->value();
    }
    if (request->hasParam("admin", true)) {
      admin = request->getParam("admin", true)->value();
    }

    Serial.println("User Profile Updated:");
    Serial.println("Username: " + name);
    Serial.println("Password: " + word);
    Serial.println("Email: " + mail);
    Serial.println("Phone Number: " + phone);
    Serial.print("PIN: ");
    Serial.println(pinStr);
    Serial.println("Admin: " + String(admin));

    for (int i = 0; i < 6; i++) {
      if (userid[i].username == "") {
        userid[i].username = name;
        userid[i].password = word;
        userid[i].email = mail;
        userid[i].phonenumber = phone;
        userid[i].admin = admin;
        pinStr.toCharArray(userid[i].pin, 6);
        saveUserData(i);
        request->send(200, "text/plain", "User added successfully");
        return;
      }
    }
  });

  server.on("/delete-user", HTTP_DELETE, [](AsyncWebServerRequest *request) {
    if (request->hasParam("index")) {
      int index = request->getParam("index")->value().toInt();
      if (index >= 0 && index < 6) {
        userid[index] = user();  // Clear user data
        saveUserData(index);
        request->send(200, "text/plain", "User deleted successfully");
      } else {
        request->send(400, "text/plain", "Invalid index");
      }
    } else {
      request->send(400, "text/plain", "Missing index parameter");
    }
  });

  server.on(
    "/post", HTTP_POST, [](AsyncWebServerRequest *request) {},
    NULL, [](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {
      //Handling function implementation
      for (size_t i = 0; i < len; i++) {
        Serial.write(data[i]);
      }
    });

  server.on(
    "/modify-user", HTTP_POST, [](AsyncWebServerRequest *request) {},
    NULL, [](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {
      // Allocate a temporary JsonDocument
      const size_t capacity = JSON_OBJECT_SIZE(6) + 200;
      DynamicJsonDocument doc(capacity);

      // Create a static buffer to hold the incoming data
      static String requestBody;

      // Append the incoming data to the requestBody
      for (size_t i = 0; i < len; i++) {
        requestBody += (char)data[i];
      }

      // If this is the last chunk of data
      if (index + len == total) {
        // Log the request body
        Serial.println("Request Body: " + requestBody);

        // Parse JSON object
        DeserializationError error = deserializeJson(doc, requestBody);
        if (error) {
          Serial.print(F("deserializeJson() failed: "));
          Serial.println(error.f_str());
          request->send(400, "text/plain", "Invalid JSON");
          requestBody = "";  // Clear the buffer
          return;
        }

        // Extract values
        int index = doc["index"];
        if (index >= 0 && index < 6) {
          userid[index].username = doc["username"].as<String>();
          userid[index].password = doc["password"].as<String>();
          userid[index].email = doc["email"].as<String>();
          userid[index].phonenumber = doc["phonenumber"].as<String>();
          strcpy(userid[index].pin, doc["pin"].as<String>().c_str());
          userid[index].admin = doc["admin"].as<bool>();
          saveUserData(index);
          request->send(200, "text/plain", "User modified successfully");
        } else {
          request->send(400, "text/plain", "Invalid index");
        }

        // Clear the buffer after processing
        requestBody = "";
      }
    });

  server.on("/device-info", HTTP_GET, [](AsyncWebServerRequest *request) {
    DynamicJsonDocument doc(1024);

    doc["deviceModel"] = esp32Info.deviceModel;
    doc["chipID"] = esp32Info.chipID;
    doc["chipCores"] = esp32Info.chipCores;
    doc["MAC_Address"] = esp32Info.MAC_Address;
    doc["firmware"] = esp32Info.firmware;
    doc["ssid"] = esp32Info.ssid;
    doc["wifi_password"] = esp32Info.wifi_password;
    doc["signalstrength"] = esp32Info.signalstrength;
    doc["hostname"] = esp32Info.hostname;
    doc["IP"] = esp32Info.IP.toString();
    doc["wificonnected"] = esp32Info.wificonnected;
    doc["uptime"] = millis();
    doc["gmtoffset"] = esp32Info.gmtoffset;
    doc["daylightoffset"] = esp32Info.daylightoffset;
    doc["interval"] = esp32Info.interval;
    doc["setup"] = esp32Info.setup;
    doc["time"] = getLocalTimeStr();

    String response;
    serializeJson(doc, response);
    request->send(200, "application/json", response);
  });

  server.on("/module-status", HTTP_GET, [](AsyncWebServerRequest *request) {
    DynamicJsonDocument doc(1024);  // Create a JSON document with sufficient memory

    JsonObject sdCard = doc.createNestedObject("sdCard");
    sdCard["module"] = "SD Card";
    if (SD.begin()) {
      sdCard["status"] = "Connected";
      sdCard["ready"] = true;
    } else {
      sdCard["status"] = "Disconnected";
      sdCard["ready"] = false;
    }

    // AI Sensor status (pseudo-code)
    JsonObject aiSensor = doc.createNestedObject("aiSensor");
    aiSensor["module"] = "AI Sensor";
    if (!AI.invoke(1, false, false)) {
      aiSensor["status"] = "Connected";
      aiSensor["ready"] = true;
    } else {
      aiSensor["status"] = "Disconnected";
      aiSensor["ready"] = false;
    }

    // nRF45 status
    JsonObject nrf45 = doc.createNestedObject("nrf45");
    nrf45["module"] = "nRF45";
    if (false) {
      nrf45["status"] = "Connected";
      nrf45["ready"] = true;
    } else {
      nrf45["status"] = "Disconnected";
      nrf45["ready"] = false;
    }

    // Unihiker status
    JsonObject unihiker = doc.createNestedObject("unihiker");
    unihiker["module"] = "Unihiker";
    if (false) {  //Need to update
      unihiker["status"] = "Connected";
      unihiker["ready"] = true;
    } else {
      unihiker["status"] = "Disconnected";
      unihiker["ready"] = false;
    }

    String json;
    serializeJson(doc, json);
    request->send(200, "application/json", json);
  });

  server.on("/resestxyz", HTTP_GET, [](AsyncWebServerRequest *request) {
    //Check user name and password and admin status
    if (true) {
      //clear SD card.
      if (SD.begin(SD_CS_PIN)) {
        Serial.println("SD card initialized to be formated");
        File root = SD.open("/");
        while (true) {
          File entry = root.openNextFile();
          if (!entry) {
            break;
          }
          if (entry.isDirectory()) {
            entry.close();
            continue;
          }
          Serial.print("Deleting file: ");
          Serial.println(entry.name());
          SD.remove(entry.name());
          entry.close();
        }
        root.close();
      } else {
        Serial.println("Failed to initialize SD card");
      }
      //clear preferences
      clearAllPreferences();
      Serial.println("Preferences cleared. Rebooting ESP32...");
      delay(5000);
      rebootESP("Reset Requested");
    }

    request->send(200, "text/html", "Reset Requested");
  });

  ElegantOTA.begin(&server);
  WebSerial.begin(&server);
  WebSerial.onMessage([&](uint8_t *data, size_t len) {
    Serial.printf("Received %lu bytes from WebSerial: ", len);
    Serial.write(data, len);
    Serial.println();
    WebSerial.println("Received Data...");
    String d = "";
    for (size_t i = 0; i < len; i++) {
      d += char(data[i]);
    }
    WebSerial.println(d);
  });
  server.onRequestBody(handleBody);
  server.begin();
  Serial.print("Web Server Ready! Use 'http://");
  Serial.print(esp32Info.IP);
  Serial.println("' to connect");
}

void loop() {
  if (streaming)  // Streaming imgages
    while (streaming) {
      if (!AI.invoke(1, false, true)) {
        if (AI.last_image().length() > 0) {
          lastImage = AI.last_image();
          // Serial.println("Captured Image: " + lastImage);
        }
      }
    }
  else if (!AI.invoke()) {  // if sensor sees person then take snapshots for 30 seconds as according to recordGrove
    for (int i = 0; i < AI.boxes().size(); i++)
      if (AI.boxes()[i].score > 70) {
        alert("Person detected. 30sec recording to begin.");
        recordGrove();
      }
  }
  ElegantOTA.loop();
  WebSerial.loop();
}

void rebootESP(String message) {
  logSD("Rebooting ESP32: ");
  logSD(message.c_str());
  ESP.restart();
}

void listDirContents(File dir, String path, String &returnText, bool ishtml) {
  File file = dir.openNextFile();
  while (file) {
    if (file.isDirectory()) {
      String dirName = path + "/" + String(file.name());
      if (ishtml) {
        returnText += "<tr><td><b>Directory:</b> " + dirName + "</td><td></td><td></td><td></td></tr>";
      } else {
        returnText += "Directory: " + dirName + "\n";
      }
      listDirContents(file, dirName, returnText, ishtml);  // Recursively list the contents of the directory
    } else {
      if (ishtml) {
        returnText += "<tr align='left'><td>" + path + "/" + String(file.name()) + "</td><td>" + humanReadableSize(file.size()) + "</td>";
        returnText += "<td><button onclick=\"downloadDeleteButton('" + path + "/" + String(file.name()) + "', 'download')\">Download</button></td>";
        returnText += "<td><button onclick=\"downloadDeleteButton('" + path + "/" + String(file.name()) + "', 'delete')\">Delete</button></td></tr>";
      } else {
        returnText += "File: " + path + "/" + String(file.name()) + " Size: " + humanReadableSize(file.size()) + "\n";
      }
    }
    file = dir.openNextFile();
  }
}

// Function to list all files and directories on the SD card
String listFiles(bool ishtml) {
  String returnText = "";
  Serial.println("Listing files stored on SD card");

  File root = SD.open("/");
  if (ishtml) {
    returnText += "<table><tr><th align='left'>Name</th><th align='left'>Size</th><th></th><th></th></tr>";
  }
  listDirContents(root, "", returnText, ishtml);  // Call the recursive function
  if (ishtml) {
    returnText += "</table>";
    Serial.println(returnText);
  }
  root.close();

  return returnText;
}

String humanReadableSize(const size_t bytes) {  // Make size of files human readable
  if (bytes < 1024)
    return String(bytes) + " B";
  else if (bytes < (1024 * 1024))
    return String(bytes / 1024.0) + " KB";
  else if (bytes < (1024 * 1024 * 1024))
    return String(bytes / 1024.0 / 1024.0) + " MB";
  else
    return String(bytes / 1024.0 / 1024.0 / 1024.0) + " GB";
}

String getContentType(String filename) {  // need to remove if not using
  if (filename.endsWith(".htm"))
    return "text/html";
  else if (filename.endsWith(".html"))
    return "text/html";
  else if (filename.endsWith(".css"))
    return "text/css";
  else if (filename.endsWith(".js"))
    return "application/javascript";
  else if (filename.endsWith(".png"))
    return "image/png";
  else if (filename.endsWith(".gif"))
    return "image/gif";
  else if (filename.endsWith(".jpg"))
    return "image/jpeg";
  else if (filename.endsWith(".ico"))
    return "image/x-icon";
  else if (filename.endsWith(".xml"))
    return "text/xml";
  else if (filename.endsWith(".pdf"))
    return "application/pdf";
  else if (filename.endsWith(".zip"))
    return "application/zip";
  else if (filename.endsWith(".gz"))
    return "application/x-gzip";
  else if (filename.endsWith(".txt"))
    return "text/plain";
  return "application/octet-stream";  // Default binary type
}

bool initSDCard() {
  if (!SD.begin(SD_CS_PIN)) {
    Serial.println("Card Mount Failed");
    return 0;
  }
  uint8_t cardType = SD.cardType();
  if (cardType == CARD_NONE) {
    Serial.println("No SD card attached");
    return 0;
  }
  Serial.print("SD Card Type: ");
  if (cardType == CARD_MMC) {
    Serial.println("MMC");
  } else if (cardType == CARD_SD) {
    Serial.println("SDSC");
  } else if (cardType == CARD_SDHC) {
    Serial.println("SDHC");
  } else {
    Serial.println("UNKNOWN");
  }
  uint64_t cardSize = SD.cardSize() / (1024 * 1024);
  Serial.printf("SD Card Size: %lluMB\n", cardSize);
  return 1;
}

void writeFile(fs::FS &fs, const char *path, const char *message) {
  File file = fs.open(path, FILE_WRITE);
  if (!file) {
    Serial.printf("Failed to open file for writing: %s\n", path);
    return;
  }
  if (file.print(message)) {
    // Serial.println("File written");
  } else {
    // Serial.println("Write failed");
  }
  file.close();
}

void appendFile(fs::FS &fs, const char *path, const char *message) {
  File file = fs.open(path, FILE_APPEND);
  if (!file) {
    Serial.printf("Failed to open file for appedning: %s\n", path);
    return;
  }
  if (file.print(message)) {
    // Serial.println("Message appended");
  } else {
    // Serial.println("Append failed");
  }
  file.close();
}

void recordGrove() {
  previousMillis = millis();
  unsigned long currentMillis = previousMillis;
  struct tm timeinfo;
  char filename[40];
  char timestamp[32];
  getLocalTime(&timeinfo);
  snprintf(filename, sizeof(filename), "/recordings/%04d-%02d-%02d-%02d-%02d-%02d.txt", timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday, timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
  writeFile(SD, filename, "Initial log\n");
  String dataToAppend = "";  // Initialize an empty string to accumulate data

  while (millis() - previousMillis < esp32Info.interval) {
    if (!AI.invoke(1, false, true))
      if (AI.last_image().length() > 0) {
        dataToAppend = AI.last_image() + "\n";  // Accumulate image data
        lastImage = dataToAppend;
        appendFile(SD, filename, dataToAppend.c_str());
      }
    currentMillis = millis();
  }
}

void alert(const char *message) {  // Function to append a message to an alert file
  char path[64];
  snprintf(path, sizeof(path), "/alerts/%04d-%02d.txt", year(), month());
  createDirectories(path);
  Serial.printf("Appending to alert file: %s\n", path);

  File file = SD.open(path, FILE_APPEND);
  if (!file) {
    Serial.println("Failed to open alert file for appending");
    return;
  }
  char timestamp[32];
  struct tm timeinfo;
  if (getLocalTime(&timeinfo)) {
    snprintf(timestamp, sizeof(timestamp), "%04d-%02d-%02d %02d:%02d:%02d ", timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday, timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
    file.print(timestamp);
  }

  String messageWithNewline = String(message) + "\n";
  if (file.print(messageWithNewline))
    Serial.println("Alert message appended");
  else
    Serial.println("Append to alert file failed");
  file.close();
}

void logSD(const char *message, bool serial) {  // Function to append a message to a log file
  if (serial)
    Serial.println(message);
  char path[64];
  snprintf(path, sizeof(path), "/device/logs/%04d-%02d.txt", year(), month());
  createDirectories(path);
  Serial.printf("Appending to log file: %s\n", path);

  File file = SD.open(path, FILE_APPEND);
  if (!file) {
    Serial.println("Failed to open log file for appending");
    return;
  }

  char timestamp[32];
  struct tm timeinfo;
  if (getLocalTime(&timeinfo)) {
    snprintf(timestamp, sizeof(timestamp), "%04d-%02d-%02d %02d:%02d:%02d ", timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday, timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
    file.print(timestamp);
  }

  String messageWithNewline = String(message) + "\n";
  if (file.print(messageWithNewline))
    Serial.println("Log message appended");
  else
    Serial.println("Append to log file failed");
  file.close();
}

int year() {  // Helper functions to get the current year
  struct tm timeinfo;
  if (getLocalTime(&timeinfo)) {
    return timeinfo.tm_year + 1900;
  }
  return 1970;  // Default year if time is not available
}

int month() {  // Helper functions to get the current month
  struct tm timeinfo;
  if (getLocalTime(&timeinfo)) {
    return timeinfo.tm_mon + 1;
  }
  return 1;  // Default month if time is not available
}

String getLocalTimeStr() {  // Helper function to get the current time as a string
  struct tm timeinfo;
  if (getLocalTime(&timeinfo)) {
    char timeString[32];
    strftime(timeString, sizeof(timeString), "%Y-%m-%d %H:%M:%S", &timeinfo);
    return String(timeString);
  }
  return "1970-01-01 00:00:00";  // Default time if time is not available
}

void createDirectories(const char *path) {  // Function to create directories if they do not exist
  char temp[64];
  char *pos = temp;
  snprintf(temp, sizeof(temp), "%s", path);

  for (pos = temp + 1; *pos; pos++) {
    if (*pos == '/') {
      *pos = '\0';
      SD.mkdir(temp);
      *pos = '/';
    }
  }
}

void setTime(const char *tz = "America/Chicago") {
  setenv("TZ", tz, 1);
  tzset();
}

String getInterfaceMacAddress(esp_mac_type_t interface) {
  String mac = "";
  unsigned char mac_base[6] = { 0 };
  if (esp_read_mac(mac_base, interface) == ESP_OK) {
    char buffer[18];  // 6*2 characters for hex + 5 characters for colons + 1 character for null terminator
    sprintf(buffer, "%02X:%02X:%02X:%02X:%02X:%02X", mac_base[0], mac_base[1], mac_base[2], mac_base[3], mac_base[4], mac_base[5]);
    mac = buffer;
  }
  return mac;
}

bool setupWiFi(const char *newSSID, const char *newPassword) {
  bool connected = false;
  int attempts;

  if (WiFi.getMode() == WIFI_AP) {  // Switch from AP mode to STA mode if necessary
    Serial.println("Currently in AP mode. Switching to STA mode...");
    WiFi.softAPdisconnect(true);
    delay(3000);
  }

  auto attemptConnection = [](const char *ssid, const char *password) {  // Function to attempt connection to a given Wi-Fi network
    WiFi.begin(ssid, password);
    int attempts = 0;
    while (WiFi.status() != WL_CONNECTED && attempts < 20) {
      delay(500);
      attempts++;
    }
    if (WiFi.status() == WL_CONNECTED) {
      WiFi.setHostname(host);
      esp32Info.ssid = ssid;
      esp32Info.wifi_password = password;
      esp32Info.signalstrength = WiFi.RSSI();
      esp32Info.IP = WiFi.localIP();
      esp32Info.MAC_Address = getInterfaceMacAddress(ESP_MAC_WIFI_STA);
      esp32Info.hostname = WiFi.getHostname();
      setTime("America/Chicago");
      configTime(esp32Info.gmtoffset, esp32Info.daylightoffset, "pool.ntp.org");
      return true;
    } else return false;
  };

  if (WiFi.status() == WL_CONNECTED) {  // If already connected, disconnect and try new network
    logSD("Currently connected to Wi-Fi. Disconnecting...");
    WiFi.disconnect();
    delay(3000);
    if (attemptConnection(newSSID, newPassword)) {
      logSD("Connected to new Wi-Fi network.");
      return true;
    } else {  // Retrieve previous network credentials
...

This file has been truncated, please download it to see its full contents.

index.html

HTML
<!doctype html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Project Postman</title>
  <link rel="stylesheet" href="/style.css">
  <link rel="icon" href="/favicon.ico" type="image/x-icon">
</head>

<body>
  <!-- Side navigation -->
  <div class="sidenav">
    <a href="#main">Main</a>
    <a href="#recordings">Recordings</a>
    <a href="#packages">Packages</a>
    <button class="dropdown-btn">Settings
      <i class="fa fa-caret-down"></i>
    </button>
    <div class="dropdown-container">
      <a href="#logs" class="sub-settings">Logs</a>
      <a href="#manage-users" class="sub-settings">Manage Users</a>
      <a href="#wifi" class="sub-settings">Device Information</a>
      <a href="#serial-messages" class="sub-settings">Software Serial Messages</a>
      <a href="#report-problem" class="sub-settings">Report a Problem</a>
      <a href="#update-firmware" class="sub-settings">Update Firmware</a>
    </div>
    <div class="auth-box">
      <a href="#login" id="login-btn">Login</a>
      <div id="logout-box">
        <a href="#logout" id="logout-btn">Logout</a>
      </div>
    </div>
  </div>

  <!-- Main content -->
  <div class="main">
    <!-- Main section -->
    <div id="main" class="content-section">
      <h2>Live Stream</h2>
      <div class="stream-container">
        <div class="stream-placeholder">
          <img id="stream" src="" alt="Live Stream" />
        </div>
        <div class="stream-controls">
          <button id="stream-btn">Start Stream</button>
          <button id="lock-btn">Unlock</button>
        </div>
      </div>
      <div class="alerts">
        <h3>Alerts</h3>
        <div class="alerts-box" id="alerts-box">
          <!-- Alert messages will appear here -->
          <p>No alerts to show.</p>
        </div>
      </div>
    </div>

    <!-- Recordings section -->
    <div id="recordings" class="content-section">
      <h2>Recordings</h2>
      <div class="stream-container">
        <div class="stream-placeholder">
          <img id="recordings-image" src="" alt="Recording Image">
        </div>
        <div class="stream-controls">
          <!-- Your control buttons here -->
        </div>
      </div>
      <div id="recordings-list" class="recordings-list">
        <!-- Recording files will be listed here -->
      </div>
    </div>

    <!-- Packages section -->
    <div id="packages" class="content-section">
      <h2>Packages</h2>
      <div class="feature-placeholder">
        <p>Feature Coming Soon</p>
      </div>
    </div>

    <!-- Logs section -->
    <div id="logs" class="content-section">
      <h2>Logs</h2>
      <div id="logs-list" class="recordings-list">
        <!-- Recording files will be listed here -->
      </div>
    </div>

    <!-- Manage Users section -->
    <div id="manage-users" class="content-section">
      <h2>Manage Users</h2>
      <!-- Edit current user info -->
      <div id="currentUserEdit" class="user-info">
        <label for="currentUserName">Name:</label>
        <input type="text" id="currentUserName" placeholder="John Doe">

        <label for="currentUserPermission">Permission:</label>
        <input type="text" id="currentUserPermission" placeholder="Admin">

        <label for="currentUserPhone">Phone Number:</label>
        <input type="text" id="currentUserPhone" placeholder="123-456-7890">

        <label for="currentUserEmail">Email:</label>
        <input type="email" id="currentUserEmail" placeholder="john.doe@example.com">

        <label for="currentUserPin">Pin Code:</label>
        <input type="text" id="currentUserPin" placeholder="****">

        <button id="saveCurrentUserBtn">Save Changes</button>
        <div class="usersTable">
          <!-- Table for displaying users -->
          <table id="userTable">
            <thead>
              <tr>
                <th>Username</th>
                <th>Phone Number</th>
                <th>Email</th>
                <th>Password</th>
                <th>Pin Code</th>
                <th>Permission</th>
                <th>Actions</th>
              </tr>
            </thead>
            <tbody>
              <tr id="user0">
                <td></td>
                <td></td>
                <td></td>
                <td></td>
                <td></td>
                <td></td>
                <td></td>
              </tr>
              <tr id="user1">
                <td></td>
                <td></td>
                <td></td>
                <td></td>
                <td></td>
                <td></td>
                <td></td>
              </tr>
              <tr id="user2">
                <td></td>
                <td></td>
                <td></td>
                <td></td>
                <td></td>
                <td></td>
                <td></td>
              </tr>
              <tr id="user3">
                <td></td>
                <td></td>
                <td></td>
                <td></td>
                <td></td>
                <td></td>
                <td></td>
              </tr>
              <tr id="user4">
                <td></td>
                <td></td>
                <td></td>
                <td></td>
                <td></td>
                <td></td>
                <td></td>
              </tr>
              <tr id="user5">
                <td></td>
                <td></td>
                <td></td>
                <td></td>
                <td></td>
                <td></td>
                <td></td>
              </tr>
            </tbody>
          </table>
        </div>
      </div>
    </div>

    <!-- WiFi Connection section -->
    <div id="wifi" class="content-section">
      <h2>Device Information</h2>
      <div class="wifi-device-container">
        <!-- WiFi Connection details -->
        <div id="wifi-connection">
          <h3>WiFi Status</h3>
          <div id="wifi-status">
            <p>Current Status: <span id="current-status">Disconnected</span></p>
            <p>Connected to: <span id="current-network">None</span></p>
            <p>Signal Strength: <span id="signal-strength">-</span> dBm</p>
            <p>IP Address: <span id="ip-address">0.0.0.0</span></p>
            <p>Hostname: <span id="hostname">N/A</span></p>
          </div>
          <!-- Connect to a selected network -->
          <div id="wifi-connect">
            <h3>Connect to a Network</h3>
            <form onsubmit="connectToNetwork(); return false;">
              <label for="network-list">SSID:</label>
              <select id="network-list" onchange="handleSelectionChange()">
                <option value="">Select an SSID</option>
                <!-- Network options will be populated here -->
              </select>
              <input type="text" id="manual-ssid" placeholder="Or type SSID here">
              <br>
              <label for="password">Password:</label>
              <input type="password" id="password" name="password">
              <br>
              <input type="submit" id ="connect-btn"value="Connect">
            </form>
            <!-- Scan for available networks -->
            <div id="wifi-scan">
              <button onclick="scanNetworks()">Scan for Networks</button>
            </div>
          </div>
          <!-- Device Information details -->
          <div id="device-info">
            <h3>Device Information</h3>
            <p>Device Model: <span id="device-model">ESP32</span></p>
            <p>MAC Address: <span id="mac-address">00:00:00:00:00:00</span></p>
            <p>Firmware Version: <span id="firmware-version">1.0.0</span></p>
            <p>Uptime: <span id="uptime">0 days 0 hours 0 minutes</span></p>
            <button onclick="rebootESP()">Reboot ESP32S3</button>
            <button onclick="factoryReset()">Factory Reset</button>
          </div>
        </div>
      </div>
    </div>

    <div id="serial-messages" class="content-section">
      <h2>Software Serial Messages</h2>
      <div class="serial-terminal">
        <div id="serial-output" class="serial-output">  Received messages will appear here 
        </div>
        <div id="serial-input" class="serial-input">
          <textarea id="messageInput" placeholder="Type your message here..."></textarea>
          <button id="sendMessageBtn">Send</button>
        </div>
      </div>
    </div>

    <!-- Report a Problem section -->
    <div id="report-problem" class="content-section">
      <h2>Report a Problem</h2>
      <div class="contact-info">
        <p>If you encounter any issues or have questions, please reach out using the following contact information:</p>
        <ul>
          <li><strong>GitHub:</strong> <a href="https://github.com/rashawnoverton"
              target="_blank">github.com/rashawnoverton</a></li>
          <li><strong>Website:</strong> <a href="https://rashawnio.wordpress.com/"
              target="_blank">rashawnio.wordpress.com/</a></li>
          <li><strong>Hackster.io:</strong> <a href="https://www.hackster.io/rashawnisaiah"
              target="_blank">hackster.io/rashawnisaiah</a></li>
          <li><strong>Email:</strong> <a href="mailto:rashawnoverton@live.com">rashawnoverton@live.com</a></li>
        </ul>
      </div>
    </div>


    <!-- Update Firmware section -->
    <div id="update-firmware" class="content-section">
      <h2>Update Firmware</h2>
      <div class="feature-placeholder">
        <p>Feature Coming Soon</p>
      </div>
    </div>

    <!-- Login page -->
    <div id="login" class="content-section">
      <h2>Login</h2>
      <div class="login-form">
        <form>
          <label for="username">Username:</label>
          <input type="text" id="username" name="username" required>
          <label for="password">Password:</label>
          <input type="password" id="password" name="password" required>
          <button type="submit">Login</button>
        </form>
      </div>
    </div>

    <!-- Logout page -->
    <div id="logout" class="content-section">
      <h2>Logout</h2>
      <div class="logout-message">
        <p>You have been logged out.</p>
        <a href="#login" id="login-link">Login Again</a>
      </div>
    </div>
  </div>
  <script src="/script.js"></script>
</body>
</html>

style.css

CSS
:root {
  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
  line-height: 1.5;
  font-weight: 400;
  color-scheme: light dark;
  color: rgba(9, 9, 9, 0.87);
  background-color: #ffffffbe;
  font-synthesis: none;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

.sidenav {
  height: 100%;
  width: 200px;
  position: fixed;
  z-index: 1;
  top: 0;
  left: 0;
  background-color: #111;
  overflow-x: hidden;
  padding-top: 20px;
}

.sidenav a,
.dropdown-btn {
  padding: 6px 8px 6px 16px;
  text-decoration: none;
  font-size: 20px;
  color: #818181;
  display: block;
  border: none;
  background: none;
  width: 100%;
  text-align: left;
  cursor: pointer;
  outline: none;
}

.sidenav a:hover,
.dropdown-btn:hover {
  color: #f1f1f1;
}

.auth-box {
  position: absolute;
  bottom: 20px;
  width: 100%;
  text-align: center;
}

.auth-box a {
  display: inline-block;
  padding: 10px 16px;
  margin: 5px;
  color: #818181;
  text-decoration: none;
  background-color: #333;
  border-radius: 5px;
}

.auth-box a:hover {
  color: #f1f1f1;
  background-color: #555;
}

.main {
  margin-left: 200px;
  font-size: 20px;
  padding: 0px 10px;
  height: 100%;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.content-section {
  display: none;
  padding: 20px;
  background-color: #f1f1f1;
  border-radius: 5px;
  height: calc(100% - 40px);
  overflow-y: auto;
}

.content-section.active {
  display: block;
}

.feature-placeholder {
  background-color: #ddd;
  color: #333;
  padding: 20px;
  border-radius: 5px;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
}

.login-form,
.logout-message {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}

.login-form form {
  display: flex;
  flex-direction: column;
  width: 300px;
}

.login-form label {
  margin-bottom: 5px;
}

.login-form input {
  margin-bottom: 10px;
  padding: 8px;
}

.login-form button {
  padding: 10px;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 5px;
  cursor: pointer;
}

.login-form button:hover {
  background-color: #0056b3;
}

.logout-message p {
  margin-bottom: 10px;
}

.logout-message a {
  color: #007bff;
  text-decoration: none;
}

.logout-message a:hover {
  text-decoration: underline;
}

.stream-container {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  /* Aligns items to the start (left) */
  gap: 10px;
  /* Space between stream placeholder and controls */
}

.stream-placeholder {
  width: 240px;
  height: 240px;
  background-color: #000;
  /* Black background to contrast with image */
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: auto;
  /* Ensure the image doesnt overflow */
}

#stream {
  width: 100%;
  /* Ensure image fills the container */
  height: 100%;
  /* Ensure image fills the container */
  object-fit: cover;
  /* Maintain aspect ratio, cover the container */
}


.stream-controls {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 10px;
  /* Space between buttons */
  margin-top: 10px;
  /* Space above the controls */
}

.stream-controls button {
  height: 50px;
  width: 115px;
  font-size: 18px;
}

.alerts {
  margin-top: 20px;
}

.alerts h3 {
  font-size: 24px;
  margin-bottom: 10px;
}

.alerts-box {
  background-color: #f9f9f9;
  border: 1px solid #ddd;
  border-radius: 5px;
  padding: 10px;
  height: 400px;
  /* Adjust the height to your preference */
  overflow-y: auto;
  box-sizing: border-box;
  display: flex;
  flex-direction: column-reverse;
  /* Ensure the scrollbar starts at the bottom */
}

.recordings-list {
  background-color: #f9f9f9;
  border: 1px solid #ddd;
  border-radius: 5px;
  padding: 10px;
  overflow-y: auto;
  height: calc(100% - 100px);
  /* Adjust to fit your design */
}

.recordings-list a {
  display: block;
  margin: 5px 0;
  padding: 10px;
  color: #007bff;
  text-decoration: none;
  border: 1px solid #ddd;
  border-radius: 5px;
  background-color: #fff;
}

.recordings-list a:hover {
  background-color: #f1f1f1;
}

.recordings-image {
  width: 100%;
  height: auto;
  background-color: #000;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Ensure the content section container uses Flexbox for layout */
.wifi-device-container {
  display: flex;
  gap: 20px; /* Space between the two sections */
}

.wifi-device-container > div {
  flex: 1; /* Allow sections to grow and fill available space equally */
}

/* Optional: Specific styles for WiFi and Device Information sections */
#wifi, #device-info {
  padding: 20px;
  background-color: #f1f1f1;
  border-radius: 5px;
  height: auto; /* Adjust height based on content */
  width: auto;
}

/* Manage Users Section */
#manage-users {
  padding: 20px;
}

#currentUserEdit {
  margin-bottom: 20px;
}

.user-info {
  background-color: #f9f9f9;
  padding: 20px;
  border-radius: 8px;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}

.user-info h3 {
  margin-top: 0;
}

.user-info label {
  display: block;
  margin: 10px 0 5px;
  font-weight: bold;
}

.user-info input {
  width: 100%;
  padding: 10px;
  border: 1px solid #ccc;
  border-radius: 4px;
  box-sizing: border-box;
  margin-bottom: 15px;
}

#saveCurrentUserBtn {
  background-color: #4CAF50;
  color: white;
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 16px;
}

#saveCurrentUserBtn:hover {
  background-color: #45a049;
}

.usersTable {
  margin-top: 20px;
}

#userTable {
  width: 100%;
  border-collapse: collapse;
}

#userTable th, #userTable td {
  border: 1px solid #ddd;
  padding: 12px;
  text-align: left;
}

#userTable th {
  background-color: #f2f2f2;
  font-weight: bold;
}

#userTable tr:nth-child(even) {
  background-color: #f9f9f9;
}

#userTable tr:hover {
  background-color: #e2e2e2;
}

#userTable .edit-btn {
  background-color: #007bff;
  color: white;
  border: none;
  padding: 5px 10px;
  border-radius: 4px;
  cursor: pointer;
}

#userTable .edit-btn:hover {
  background-color: #0056b3;
}

/* Software Serial Messages Section */
#serial-messages {
  padding: 20px;
}

.serial-terminal {
  background-color: #f9f9f9;
  padding: 20px;
  border-radius: 8px;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}

.serial-output {
  background-color: #fff;
  border: 1px solid #ccc;
  border-radius: 4px;
  height: 300px;
  overflow-y: auto;
  padding: 10px;
  margin-bottom: 15px;
  font-family: monospace;
}

.serial-input {
  display: flex;
  flex-direction: column;
}

#messageInput {
  width: 100%;
  height: 100px;
  padding: 10px;
  border: 1px solid #ccc;
  border-radius: 4px;
  box-sizing: border-box;
  font-family: monospace;
}

#sendMessageBtn {
  margin-top: 10px;
  background-color: #007bff;
  color: white;
  padding: 10px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 16px;
}

#sendMessageBtn:hover {
  background-color: #0056b3;
}

/* Report a Problem section */
#report-problem {
  padding: 20px;
  background-color: #f9f9f9;
  border-radius: 8px;
  margin: 20px;
}

#report-problem h2 {
  font-size: 24px;
  margin-bottom: 10px;
}

.contact-info {
  font-size: 16px;
  line-height: 1.6;
}

.contact-info p {
  margin: 10px 0;
}

.contact-info a {
  color: #007BFF;
  text-decoration: none;
}

.contact-info a:hover {
  text-decoration: underline;
}

setup.html

HTML
<!DOCTYPE html>
<html lang="en">

<head>
    <title>Project Postman - WiFi Manager</title>
    <link rel="icon" href="/favicon.ico" type="image/x-icon">
    <style>
        body {
            font-family: Arial, sans-serif;
            background-color: #f4f4f9;
            color: #333;
            margin: 0;
            padding: 0;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
        }

        .container {
            background-color: #fff;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
            max-width: 600px;
            width: 100%;
            text-align: center;
            justify-content: center;
        }

        h1 {
            color: #007bff;
            font-size: 24px;
            margin-bottom: 10px;
        }

        p {
            font-size: 16px;
            line-height: 1.6;
            margin-bottom: 10px;
        }

        button {
            background-color: #007bff;
            color: #fff;
            border: none;
            padding: 10px 20px;
            font-size: 16px;
            border-radius: 5px;
            cursor: pointer;
            transition: background-color 0.3s ease;
            margin-top: 10px;
            margin-bottom: 10px;
        }

        button:hover {
            background-color: #0056b3;
        }

        .slide {
            display: none;
        }

        .slide.active {
            display: block;
        }

        .container {
            display: flex;
            flex-direction: column;
        }

        .check-item {
            display: flex;
            align-items: center;
            margin-bottom: 10px;

        }

        .round {
            position: relative;
            margin-right: 10px;
            /* Space between the checkbox and the label */
        }

        .container {
            display: flex;
            flex-direction: column;
            justify-content: center;
        }

        .scrollable-content {
            max-height: fit-content;
            max-width: fit-content;
            /* Adjust this value as needed */
            overflow-y: auto;
            padding-right: 10px;
            /* Optional: Add some padding to avoid scrollbar overlap */
        }

        .check-item {
            display: flex;
            align-items: center;
            margin-bottom: 10px;
        }

        .round {
            position: relative;
            margin-right: 10px;
            /* Space between the checkbox and the info */
            display: flex;
            align-items: center;
            /* Centering the checkbox vertically */
        }

        .round label {
            background-color: #fff;
            border: 1px solid #ccc;
            border-radius: 50%;
            cursor: pointer;
            height: 28px;
            width: 28px;
            display: flex;
            align-items: center;
            justify-content: center;
        }

        .round label:after {
            border: 2px solid #fff;
            border-top: none;
            border-right: none;
            content: "";
            height: 6px;
            width: 12px;
            transform: rotate(-45deg);
            opacity: 0;
            position: absolute;
            top: 7px;
            left: 7px;
        }

        .round input[type="checkbox"] {
            visibility: hidden;
        }

        .round input[type="checkbox"]:checked+label {
            background-color: #66bb6a;
            border-color: #66bb6a;
        }

        .round input[type="checkbox"]:checked+label:after {
            opacity: 1;
        }

        .check-info {
            display: flex;
            flex-direction: row;
            justify-content: center;
        }

        .check-label,
        .status {
            margin: 0;
            text-align: center;
            flex-direction: row;
        }

        /* Mobile Styles */
        @media (max-width: 600px) {
            .container {
                padding: 10px;
            }

            h1 {
                font-size: 20px;
            }

            p {
                font-size: 14px;
            }

            button {
                font-size: 14px;
                padding: 8px 16px;
            }
        }

        /* Style for usersTable */
        #usersTable {
            width: 100%;
            border-collapse: collapse;
        }

        #usersTable th,
        #usersTable td {
            border: 1px solid #ddd;
            padding: 8px;
        }

        #usersTable th {
            background-color: #007bff;
            color: white;
            text-align: center;
        }

        #usersTable tr:nth-child(even) {
            background-color: #f2f2f2;
        }

        #usersTable tr:hover {
            background-color: #ddd;
        }

        #usersTable td {
            text-align: center;
        }
    </style>
</head>

<body>
    <div class="container">
        <!-- Slide 1: Welcome and Start Setup -->
        <div class="slide active" id="slide1">
            <h1>Welcome to Project Postman</h1>
            <p>Click the button below to start the setup process.</p>
            <button onclick="nextSlide()">Start Setup</button>
        </div>

        <div class="slide" id="slide2">
            <!-- Slide 2 content -->
            <h1>Sensors and Modules</h1>
            <h2>Device Status</h2>
            <div class="check-item">
                <div class="round">
                    <input type="checkbox" id="sd-card-checkbox" disabled>
                    <label for="sd-card-checkbox"></label>
                </div>
                <div class="check-info">
                    <h4 class="check-label">SD Card</h4>
                    <p class="status"><strong>.........................................................</strong></p>
                    <p class="status" id="sd-card-status"></p>
                </div>
            </div>
            <div class="check-item">
                <div class="round">
                    <input type="checkbox" id="grove-ai-checkbox" disabled>
                    <label for="grove-ai-checkbox"></label>
                </div>
                <div class="check-info">
                    <h4 class="check-label">AI Sensor</h4>
                    <p class="status"><strong>.......................................................</strong></p>
                    <p class="status" id="grove-ai-status"></p>
                </div>
            </div>
            <div class="check-item">
                <div class="round">
                    <input type="checkbox" id="nrf45-checkbox" disabled>
                    <label for="nrf45-checkbox"></label>
                </div>
                <div class="check-info">
                    <h4 class="check-label">nRF45</h4>
                    <p class="status"><strong>............................................................</strong></p>
                    <p class="status" id="nrf45-status"></p>
                </div>
            </div>
            <div class="check-item">
                <div class="round">
                    <input type="checkbox" id="unihiker-checkbox" disabled>
                    <label for="unihiker-checkbox"></label>
                </div>
                <div class="check-info">
                    <h4 class="check-label">Unihiker</h4>
                    <p class="status"><strong>..........................................................</strong></p>
                    <p class="status" id="unihiker-status"></p>
                </div>
            </div>
            <button onclick="previousSlide()">Previous</button>
            <button onclick="nextSlide()">Next</button>
        </div>

        <!-- Slide 3: Device Info and WiFi Setup -->
        <div id="slide3" class="slide">
            <h1>Device Info and WiFi Setup</h1>
            <!-- Display device info here -->
            <p>Device Model: <span id="device-model">%DeviceModel%</span></p>
            <p>Firmware Version: <span id="firmware-version">%Version%</span></p>
            <!-- Add WiFi setup form here -->
            <form id="wifiSetupForm" onsubmit="connectToNetwork(); return false;">
                <label for="network-list">SSID:</label>
                <select id="network-list" onchange="handleSelectionChange()">
                    <option value="">Select an SSID</option>
                    <!-- Network options will be populated here -->
                </select>
                <input type="text" id="manual-ssid" placeholder="Or type SSID here">
                <br>
                <label for="password">Password:</label>
                <input type="password" id="password" name="password">
                <input id="show" type="checkbox" onclick="togglePasswordVisibility()">
                <label for="show">View Password</label>
                <br>
                <input type="button" onclick="scanNetworks()" value="Scan Networks">
                <input type="submit" id="connect-btn" value="Connect">
            </form>
            <p><strong>WiFi:</strong> <span id="wifi-status">$wificonnected</span></p>
            <p><strong>Signal Strength:</strong> <span id="signal-strength">$signalstrength</span></p>
            <p><strong>SSID:</strong> <span id="ssid">$ssid</span></p>
            <p><strong>WiFi Password:</strong> <span id="wifi-password">$wifipassword</span></p>
            <p><strong>IP Address:</strong> <span id="ip-address">$IPAddress</span></p>
            <p><strong>Hostname:</strong> <span id="hostname">$hostname</span></p>
            <p> <span id="date-time"><strong> %TimeDate%</strong></span></p>
            <form id="date-time-form" onsubmit="setTimezone(); return false;">
                <label for="timezone">Timezone:</label>
                <select id="timezone">
                    <option>Select Timezone</option>
                    <option value="Pacific/Midway">(GMT-11:00) Midway Island, Samoa</option>
                    <option value="Pacific/Honolulu">(GMT-10:00) Hawaii</option>
                    <option value="America/Anchorage">(GMT-09:00) Alaska</option>
                    <option value="America/Los_Angeles">(GMT-08:00) Pacific Time (US & Canada)</option>
                    <option value="America/Phoenix">(GMT-07:00) Arizona</option>
                    <option value="America/Denver">(GMT-07:00) Mountain Time (US & Canada)</option>
                    <option value="America/Chicago">(GMT-06:00) Central Time (US & Canada)</option>
                    <option value="America/New_York">(GMT-05:00) Eastern Time (US & Canada)</option>
                    <option value="America/Caracas">(GMT-04:30) Caracas</option>
                    <option value="America/Halifax">(GMT-04:00) Atlantic Time (Canada)</option>
                    <option value="America/Santo_Domingo">(GMT-04:00) Santo Domingo</option>
                    <option value="America/Argentina/Buenos_Aires">(GMT-03:00) Buenos Aires, Georgetown</option>
                    <option value="America/Sao_Paulo">(GMT-03:00) Brasilia</option>
                    <option value="America/Noronha">(GMT-02:00) Mid-Atlantic</option>
                    <option value="Atlantic/Azores">(GMT-01:00) Azores</option>
                    <option value="Atlantic/Cape_Verde">(GMT-01:00) Cape Verde Islands</option>
                    <option value="Africa/Casablanca">(GMT+00:00) Casablanca, Monrovia</option>
                    <option value="Europe/London">(GMT+00:00) London, Dublin, Edinburgh</option>
                    <option value="Europe/Paris">(GMT+01:00) Paris, Berlin, Rome, Madrid</option>
                    <option value="Europe/Athens">(GMT+02:00) Athens, Bucharest</option>
                    <option value="Africa/Cairo">(GMT+02:00) Cairo</option>
                    <option value="Asia/Jerusalem">(GMT+02:00) Jerusalem</option>
                    <option value="Asia/Baghdad">(GMT+03:00) Baghdad</option>
                    <option value="Asia/Riyadh">(GMT+03:00) Riyadh, Kuwait, Nairobi</option>
                    <option value="Asia/Tehran">(GMT+03:30) Tehran</option>
                    <option value="Asia/Dubai">(GMT+04:00) Abu Dhabi, Dubai</option>
                    <option value="Asia/Baku">(GMT+04:00) Baku, Tbilisi, Yerevan</option>
                    <option value="Asia/Kabul">(GMT+04:30) Kabul</option>
                    <option value="Asia/Karachi">(GMT+05:00) Islamabad, Karachi</option>
                    <option value="Asia/Kolkata">(GMT+05:30) Chennai, Kolkata, Mumbai, New Delhi</option>
                    <option value="Asia/Kathmandu">(GMT+05:45) Kathmandu</option>
                    <option value="Asia/Dhaka">(GMT+06:00) Dhaka</option>
                    <option value="Asia/Almaty">(GMT+06:00) Almaty, Novosibirsk</option>
                    <option value="Asia/Rangoon">(GMT+06:30) Yangon (Rangoon)</option>
                    <option value="Asia/Bangkok">(GMT+07:00) Bangkok, Hanoi, Jakarta</option>
                    <option value="Asia/Hong_Kong">(GMT+08:00) Beijing, Hong Kong</option>
                    <option value="Asia/Krasnoyarsk">(GMT+08:00) Krasnoyarsk, Kuala Lumpur</option>
                    <option value="Asia/Tokyo">(GMT+09:00) Tokyo, Seoul</option>
                    <option value="Australia/Adelaide">(GMT+09:30) Adelaide</option>
                    <option value="Australia/Sydney">(GMT+10:00) Sydney, Melbourne</option>
                    <option value="Pacific/Guam">(GMT+10:00) Guam, Port Moresby</option>
                    <option value="Australia/Brisbane">(GMT+10:00) Brisbane</option>
                    <option value="Australia/Darwin">(GMT+09:30) Darwin</option>
                    <option value="Pacific/Noumea">(GMT+11:00) Noumea, Vladivostok</option>
                    <option value="Pacific/Auckland">(GMT+12:00) Auckland, Wellington</option>
                    <option value="Pacific/Tongatapu">(GMT+13:00) Nuku'alofa</option>
                </select>
                <label for="daylight-savings">Daylight Savings</label>
                <input type="checkbox" id="daylight-savings" />
                <br>
                <label for="time-offset">Time Offset:</label>
                <input type="number" id="time-offset" name="time-offset" min="-12" max="12" step="1" />
                <br>
                <input type="submit" id="set-datetime-btn" value="Set Date and Time" />
                <br>
            </form>
            <button onclick="previousSlide()">Previous</button>
            <button onclick="nextSlide()">Next</button>
        </div>

        <!-- Slide 4: User Profile Setup -->
        <div id="slide4" class="slide">
            <h1>User Profile Setup</h1>
            <!-- Add user profile setup form here -->
            <div class="scrollable-content">
                <form id="userProfileForm" onsubmit="addUser(event)">
                    <label for="username">Username:</label>
                    <input type="text" id="username" name="username" required>
                    <br>
                    <label for="user-password">Password:</label>
                    <input type="password" id="user-password" name="password" required>
                    <br>
                    <label for="email">Email:</label>
                    <input type="email" id="email" name="email" required>
                    <br>
                    <label for="phonenumber">Phone Number:</label>
                    <input type="tel" id="phonenumber" name="phonenumber" required>
                    <br>
                    <label for="pin">PIN (6-digits):</label>
                    <input type="text" id="pin" name="pin" pattern="[0-9]{6}" required>
                    <br>
                    <label for="admin">Admin:</label>
                    <input type="checkbox" id="admin" name="admin">
                    <br>
                    <button type="submit">Add User</button>
                </form>

                <table id="usersTable" border="1">
                    <thead>
                        <tr>
                            <th>ID</th>
                            <th>Username</th>
                            <th>Password</th>
                            <th>Email</th>
                            <th>Phone Number</th>
                            <th>PIN</th>
                            <th>Admin</th>
                            <th>Actions</th>
                        </tr>
                    </thead>
                    <tbody id="usersTableBody">
                        <tr id="user0">
                            <td>0</td>
                            <td></td>
                            <td></td>
                            <td></td>
                            <td></td>
                            <td></td>
                            <td></td>
                            <td></td>
                        </tr>
                        <tr id="user1">
                            <td>1</td>
                            <td></td>
                            <td></td>
                            <td></td>
                            <td></td>
                            <td></td>
                            <td></td>
                            <td></td>
                        </tr>
                        <tr id="user2">
                            <td>2</td>
                            <td></td>
                            <td></td>
                            <td></td>
                            <td></td>
                            <td></td>
                            <td></td>
                            <td></td>
                        </tr>
                        <tr id="user3">
                            <td>3</td>
                            <td></td>
                            <td></td>
                            <td></td>
                            <td></td>
                            <td></td>
                            <td></td>
                            <td></td>
                        </tr>
                        <tr id="user4">
                            <td>4</td>
                            <td></td>
                            <td></td>
                            <td></td>
                            <td></td>
                            <td></td>
                            <td></td>
                            <td></td>
                        </tr>
                        <tr id="user5">
                            <td>5</td>
                            <td></td>
                            <td></td>
                            <td></td>
                            <td></td>
                            <td></td>
                            <td></td>
                            <td></td>
                        </tr>
                    </tbody>
                </table>
                <button onclick="previousSlide()">Previous</button>
                <button onclick="nextSlide()">Next</button>
            </div>
        </div>

        <!-- Slide 5: Finish Setup -->
        <div id="slide5" class="slide">
            <h1>Finish Setup</h1>
            <p>Please confirm your information:</p>
            <!-- Display confirmed information here -->
            <p>Device Model: <span id="confirmed-device-model">%DeviceModel%</span></p>
            <p>Firmware Version: <span id="confirmed-firmware-version">%Version%</span></p>
            <p>SSID: <span id="confirmed-ssid">$data.ssid</span></p>
            <p>WiFi Password: <span id="confirmed-wifi-password">$wifi_password</span></p>
            <p>IP Address: <span id="confirmed-ip-address">$data.IP</span></p>
            <p>Hostname: <span id="confirmed-hostname">$hostname</span></p>
            <p>Timezone: <span id="confirmed-timezone">$selected_timezone</span></p>
            <p>Daylight Savings: <span id="confirmed-daylight-savings">$daylight_savings</span></p>
            <p>Time Offset: <span id="confirmed-time-offset">$time_offset</span></p>
            <p>Date and Time: <span id="confirmed-date-time">$TimeDate</span></p>
            <br>
            <p>Click the button below to finish the setup:</p>
            <button onclick="previousSlide()">Previous</button>
            <button onclick="rebootESP()">Finish Setup and Reboot</button>
        </div>

        <script>
            document.addEventListener('DOMContentLoaded', (event) => {
                loadDeviceInfo();
            });

            let currentSlide = 0;
            const slides = document.querySelectorAll('.slide');

            function showSlide(index) {
                slides.forEach((slide, i) => {
                    slide.classList.toggle('active', i === index);
                });
                if (index === 1) {
                    updateDeviceStatus();
                }
                if (index === 3) {
                    loadUsers();
                }
                if (index === 4) {
                    loadFinishInfo();
                }
            }

            function nextSlide() {
                currentSlide = (currentSlide + 1) % slides.length;
                showSlide(currentSlide);
            }

            function previousSlide() {
                currentSlide = (currentSlide - 1 + slides.length) % slides.length;
                showSlide(currentSlide);
            }

            function togglePasswordVisibility() {
                const passwordInput = document.getElementById('password');
                if (passwordInput.type === 'password') {
                    passwordInput.type = 'text';
                } else {
                    passwordInput.type = 'password';
                }
            }

            function scanNetworks() { // Function to scan networks and populate the dropdown
                fetch('/scan-networks')
                    .then(response => response.json())
                    .then(networks => {
                        const networkList = document.getElementById('network-list');
                        networkList.innerHTML = '<option value="">Select an SSID</option>'; // Reset dropdown

                        networks.forEach(network => {
                            const option = document.createElement('option');
                            option.textContent = `${network.ssid} (${network.signal_strength} dBm)`;
                            option.value = network.ssid; // Set value to SSID
                            networkList.appendChild(option);
                        });
                    })
                    .catch(error => {
                        console.error('Error fetching network list:', error);
                    });
            }

            function connectToNetwork() {
                const networkList = document.getElementById('network-list');
                const manualSSID = document.getElementById('manual-ssid').value.trim();
                const ssid = networkList.value || manualSSID; // Use selected SSID or manual input
                const password = document.getElementById('password').value;

                if (!ssid) {
                    alert('Please enter or select an SSID.');
                    return;
                }

                // Send connection request to the server
                fetch('/connect', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                    },
                    body: JSON.stringify({ ssid, password }),
                })
                    .then(response => response.text())
                    .then(result => {
                        console.log('Connection result:', result);
                        // Handle connection result
                    })
                    .catch(error => {
                        console.error('Error connecting to network:', error);
                    });
            }

            function handleSelectionChange() { // Function to handle changes in the dropdown or input field
                const networkList = document.getElementById('network-list');
                const manualSSID = document.getElementById('manual-ssid');

                // If a dropdown option is selected, clear the input field
                if (networkList.value) {
                    manualSSID.value = ''; // Clear manual input
                }
            }

            function setTimezone() {
                const timezoneSelect = document.getElementById('timezone');
                const selectedTimezone = timezoneSelect.value;

                const timeOffsetInput = document.getElementById('time-offset');
                const timeOffset = timeOffsetInput.value;

                const daylightSavingsCheckbox = document.getElementById('daylight-savings');
                const daylightSavings = daylightSavingsCheckbox.checked ? 3600 : 0; // 3600 seconds = 1 hour

                const url = `/set-timezone?timezone=${encodeURIComponent(selectedTimezone)}&gmtoffset=${encodeURIComponent(timeOffset)}&daylightoffset=${encodeURIComponent(daylightSavings)}`;

                fetch(url)
                    .then(response => response.text())
                    .then(result => {
                        console.log('Timezone set result:', result);
                        // Display the result on the webpage
                        document.getElementById('date-time').textContent = result;
                    })
                    .catch(error => {
                        console.error('Error setting timezone:', error);
                    });
            }

            function loadDeviceInfo() {
                fetch('/device-info')
                    .then(response => response.json())
                    .then(data => {
                        document.getElementById('device-model').textContent = data.deviceModel;
                        document.getElementById('firmware-version').textContent = data.firmware;
                        document.getElementById('wifi-status').textContent = data.wificonnected ? 'Connected' : 'Disconnected';
                        document.getElementById('signal-strength').textContent = data.signalstrength;
                        document.getElementById('ssid').textContent = data.ssid;
                        document.getElementById('wifi-password').textContent = data.wifi_password;
                        document.getElementById('ip-address').textContent = data.IP;
                        document.getElementById('hostname').textContent = data.hostname;
                        document.getElementById('date-time').textContent = data.time;
                    })
                    .catch(error => {
                        console.error('Error fetching device info:', error);
                    });
            }

            function updateDeviceStatus() {
                fetch('/module-status')
                    .then(response => response.json())
                    .then(data => {
                        document.getElementById('sd-card-checkbox').checked = data.sdCard.status === 'Connected';
                        document.getElementById('sd-card-status').textContent = data.sdCard.status;

                        document.getElementById('grove-ai-checkbox').checked = data.aiSensor.status === 'Connected';
                        document.getElementById('grove-ai-status').textContent = data.aiSensor.status;

                        document.getElementById('nrf45-checkbox').checked = data.nrf45.status === 'Connected';
                        document.getElementById('nrf45-status').textContent = data.nrf45.status;

                        document.getElementById('unihiker-checkbox').checked = data.unihiker.status === 'Connected';
                        document.getElementById('unihiker-status').textContent = data.unihiker.status;
                    })
                    .catch(error => console.error('Error fetching device status:', error));
            }

            function rebootESP() {
                if (confirm("Are you sure you want to reboot the ESP32S3?")) {
                    fetch('/finish')
                        .then(response => response.text())
                        .then(text => console.log('Reboot command sent:', text))
                        .catch(error => console.error('Error sending reboot command:', error));
                } else {
                    console.log('Reboot canceled.');
                }
            }

            function loadUsers() {
                fetch('/manage-users')
                    .then(response => response.json())
                    .then(data => {
                        const usersTableBody = document.getElementById('usersTableBody');
                        usersTableBody.innerHTML = ''; // Clear existing rows

                        data.users.forEach((user, index) => {
                            const row = document.createElement('tr');
                            row.innerHTML = `
                        <td>${index}</td>
                        <td>${user.username}</td>
                        <td>${user.password}</td>
                        <td>${user.email}</td>
                        <td>${user.phonenumber}</td>
                        <td>${user.pin}</td>
                        <td>${user.admin ? 'Yes' : 'No'}</td>
                        <td>
                          <button onclick="deleteUser(${index})">Delete</button>
                          <button onclick="modifyUser(${index})">Modify</button>
                        </td>
                      `;
                            usersTableBody.appendChild(row);
                        });
                    })
                    .catch(error => console.error('Error fetching user data:', error));
            }

            function addUser(event) {
                event.preventDefault(); // Prevent the default form submission

                const form = document.getElementById('userProfileForm');
                const formData = new FormData(form);

                fetch('/add-user', {
                    method: 'POST',
                    body: formData
                })
                    .then(response => response.text())
                    .then(data => {
                        console.log('Form submission successful:', data);
                        console.log(data);
                        form.reset(); // Clear the form after submission
                        loadUsers(); // Reload users after adding a new user
                    })
                    .catch(error => {
                        console.error('Error:', error);
                    });
            }

            function deleteUser(index) {
                fetch(`/delete-user?index=${index}`, { method: 'DELETE' })
                    .then(response => response.text())
                    .then(result => {
                        console.log(result);
                        alert(result);
                        loadUsers(); // Reload users after deletion
                    })
                    .catch(error => console.error('Error deleting user:', error));
            }

            function modifyUser(index) {
                const row = document.querySelector(`#usersTableBody tr:nth-child(${index + 1})`);
                const cells = row.querySelectorAll('td');

                // Replace cell content with input fields
                cells[1].innerHTML = `<input type="text" value="${cells[1].innerText}" id="username-${index}">`;
                cells[2].innerHTML = `<input type="password" value="${cells[2].innerText}" id="password-${index}">`;
                cells[3].innerHTML = `<input type="email" value="${cells[3].innerText}" id="email-${index}">`;
                cells[4].innerHTML = `<input type="tel" value="${cells[4].innerText}" id="phonenumber-${index}">`;
                cells[5].innerHTML = `<input type="text" value="${cells[5].innerText}" id="pin-${index}" pattern="[0-9]{6}">`;
                cells[6].innerHTML = `<input type="checkbox" ${cells[6].innerText === 'Yes' ? 'checked' : ''} id="admin-${index}">`;

                // Replace Modify button with Save button
                cells[7].innerHTML = `
                    <button onclick="saveUser(${index})">Save</button>
                    <button onclick="cancelEdit(${index})">Cancel</button>
                `;
            }

            function saveUser(index) {
                const username = document.getElementById(`username-${index}`).value;
                const password = document.getElementById(`password-${index}`).value;
                const email = document.getElementById(`email-${index}`).value;
                const phonenumber = document.getElementById(`phonenumber-${index}`).value;
                const pin = document.getElementById(`pin-${index}`).value;
                const admin = document.getElementById(`admin-${index}`).checked;
            
                const requestBody = {index, username, password, email, phonenumber, pin, admin};
            
                // Log the body to the console
                console.log('Request Body:', JSON.stringify(requestBody));
            
                fetch('/modify-user', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                    },
                    body: JSON.stringify(requestBody),
                })
                .then(response => {
                    // Log the response status and headers
                    console.log('Response Status:', response.status);
                    console.log('Response Headers:', response.headers);
                    return response.text();
                })
                .then(result => {
                    console.log(result);
                    loadUsers(); // Reload users after modification
                })
                .catch(error => console.error('Error:', error));
            }

            function cancelEdit(index) {
                loadUsers(); // Reload users to cancel edit
            }

            function loadFinishInfo() {
                fetch('/device-info')
                    .then(response => response.json())
                    .then(data => {
                        document.getElementById('confirmed-device-model').textContent = data.deviceModel;
                        document.getElementById('confirmed-firmware-version').textContent = data.firmware;
                        document.getElementById('confirmed-ssid').textContent = data.ssid;
                        document.getElementById('confirmed-wifi-password').textContent = data.wifi_password;
                        document.getElementById('confirmed-ip-address').textContent = data.IP;
                        document.getElementById('confirmed-hostname').textContent = data.hostname;
                        document.getElementById('confirmed-timezone').textContent = data.gmtoffset; // Assuming gmtoffset is the timezone
                        document.getElementById('confirmed-daylight-savings').textContent = data.daylightoffset; // Assuming daylightoffset is daylight savings
                        document.getElementById('confirmed-time-offset').textContent = data.interval; // Assuming interval is the time offset
                        document.getElementById('confirmed-date-time').textContent = data.time;
                    })
                    .catch(error => console.error('Error fetching device info:', error));
            }
            
            // Call loadDeviceInfo when the slide is viewed
            document.getElementById('slide5').addEventListener('show', loadFinishInfo);

            // Call loadUsers when the page loads
            document.addEventListener('DOMContentLoaded', loadUsers);

            // Initialize the first slide
            showSlide(currentSlide);
        </script>
    </div>
</body>

</html>

script.js

JavaScript
function downloadDeleteButton(filePath, action) {
  var url = '/' + action + '?file=' + encodeURIComponent(filePath);
  if (action === 'download') {
    window.location.href = url;
  } else if (action === 'delete') {
    if (confirm("Are you sure you want to delete this file?")) {
      fetch(url, { method: 'DELETE' })
        .then(response => response.text())
        .then(text => {
          console.log(text);
          loadLogs(); // Refresh the Logs list after deletion
        })
        .catch(error => console.error('Error deleting file:', error));
    }
  }
}

function rebootESP() {
  if (confirm("Are you sure you want to reboot the ESP32S3?")) {
    fetch('/reboot')
      .then(response => response.text())
      .then(text => console.log('Reboot command sent:', text))
      .catch(error => console.error('Error sending reboot command:', error));
  } else {
    console.log('Reboot canceled.');
  }
}

function scanNetworks() { // Function to scan networks and populate the dropdown
  fetch('/scan-networks')
    .then(response => response.json())
    .then(networks => {
      const networkList = document.getElementById('network-list');
      networkList.innerHTML = '<option value="">Select an SSID</option>'; // Reset dropdown

      networks.forEach(network => {
        const option = document.createElement('option');
        option.textContent = `${network.ssid} (${network.signal_strength} dBm)`;
        option.value = network.ssid; // Set value to SSID
        networkList.appendChild(option);
      });
    })
    .catch(error => {
      console.error('Error fetching network list:', error);
    });
}


function handleSelectionChange() { // Function to handle changes in the dropdown or input field
  const networkList = document.getElementById('network-list');
  const manualSSID = document.getElementById('manual-ssid');

  // If a dropdown option is selected, clear the input field
  if (networkList.value) {
    manualSSID.value = ''; // Clear manual input
  }
}

// Function to handle form submission
function connectToNetwork() {
  const networkList = document.getElementById('network-list');
  const manualSSID = document.getElementById('manual-ssid').value.trim();
  const ssid = networkList.value || manualSSID; // Use selected SSID or manual input
  const password = document.getElementById('password').value;

  if (!ssid) {
    alert('Please enter or select an SSID.');
    return;
  }

  // Send connection request to the server
  fetch('/connect', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ ssid, password }),
  })
    .then(response => response.text())
    .then(result => {
      console.log('Connection result:', result);
      // Handle connection result
    })
    .catch(error => {
      console.error('Error connecting to network:', error);
    });
}

// Initialize dropdown buttons
const dropdownButtons = document.getElementsByClassName("dropdown-btn");
Array.from(dropdownButtons).forEach(button => {
  button.addEventListener("click", function () {
    this.classList.toggle("active"); // Toggle the active class to show/hide the dropdown content
    const dropdownContent = this.nextElementSibling;
    dropdownContent.style.display = dropdownContent.style.display === "block" ? "none" : "block";
  });
});

// Handle page navigation
const sections = document.querySelectorAll('.content-section');

// Function to show the section with the given ID
function showSection(id) {
  sections.forEach(section => {
    const isActive = section.id === id;
    section.classList.toggle('active', isActive);

    if (id === 'update-firmware' && isActive) {
      window.open('/update', '_blank');
    }

    if (id == 'serial-messages' && isActive) {
      window.open('/webserial', '_blank');
    }
  });
}

// Event listeners for navigation links
document.querySelectorAll('.sidenav a').forEach(link => {
  link.addEventListener('click', event => {
    event.preventDefault();
    const targetId = link.getAttribute('href').substring(1);
    showSection(targetId);
  });
});

// Initialize first section
showSection('main');

// Handle Image Stream
document.addEventListener("DOMContentLoaded", () => {
  let streaming = false;  // Flag to track streaming status
  const streamImg = document.getElementById('stream');
  const streamBtn = document.getElementById('stream-btn');
  let streamInterval = null;

  if (streamBtn) {
    // Click event for stream button
    streamBtn.addEventListener('click', () => {
      if (streaming) {
        // Stop streaming
        streamBtn.textContent = 'Start Stream';
        streaming = false;
        fetch('/stopStream');  // Send request to stop streaming

        // Clear interval to stop fetching frames
        clearInterval(streamInterval);
        streamInterval = null;

        // The last image remains displayed
      } else {
        // Start streaming
        streamBtn.textContent = 'Stop Stream';
        streaming = true;
        fetch('/startStream');  // Send request to start streaming

        // Set interval to fetch new frames
        streamInterval = setInterval(() => {
          if (streaming) {
            fetch('/stream')
              .then(response => response.text())
              .then(text => {
                if (text.length > 0) {
                  streamImg.src = 'data:image/jpeg;base64,' + text;
                } else {
                  console.log('No image received.');
                }
              })
              .catch(error => console.error('Error fetching stream:', error));
          }
        }, 50); // Fetch new frames every 50 milliseconds
      }
    });
  } else {
    console.error('Stream button not found.');
  }

  // Handle Lock/Unlock Button
  const lockBtn = document.getElementById('lock-btn');
  let locked = false;

  if (lockBtn) {
    lockBtn.addEventListener('click', () => {
      if (locked) {
        // Unlock the system
        fetch('/lock');
        lockBtn.textContent = 'Unlock';
      } else {
        // Lock the system
        fetch('/unlock');
        lockBtn.textContent = 'Lock';
      }
      locked = !locked;
    });
  } else {
    console.error('Lock button not found.');
  }

  // Fetch and display alerts
  const alertsBox = document.getElementById('alerts-box');

  function loadAlerts() {
    fetch('/alerts')
      .then(response => response.text())
      .then(text => {
        if (text.length > 0) {
          alertsBox.innerHTML = text.replace(/\n/g, '<br>'); // Replace newlines with <br>
        } else {
          alertsBox.innerHTML = '<p>No alerts to show.</p>';
        }
      })
      .catch(error => {
        console.error('Error fetching alerts:', error);
        alertsBox.innerHTML = '<p>Error loading alerts.</p>';
      });
  }

  // Initial load of alerts
  loadAlerts();

  // Refresh alerts every minute
  setInterval(loadAlerts, 60000); // Refresh every 60,000 ms (1 minute)

  // Fetch and display Logs
  const logsList = document.getElementById('logs-list');

  function loadLogs() {
    fetch('/logs')
      .then(response => response.text())
      .then(text => {
        const logs = text.split('\n').filter(file => file.trim() !== ''); // Split the text by newlines
        if (logs.length > 0) {
          logsList.innerHTML = logs;
        } else {
          logsList.innerHTML = '<p>No logs available.</p>';
        }
      })
      .catch(error => {
        console.error('Error fetching logs:', error);
        logsList.innerHTML = '<p>Error loading logs.</p>';
      });
  }

  function playRecording(filename) {
    console.log(`Playing recording: ${filename}`);
    fetch(`/play-recording?file=${filename}`)
      .then(response => response.text())
      .then(data => {
        console.log('Data received:', data);
        const frames = data.split('\n').slice(1); // Discard the first frame (title)
        const totalFrames = frames.length;
        const intervalTime = 30000 / totalFrames; // 30 seconds divided by the number of frames

        console.log(`Total frames: ${totalFrames}`);
        console.log(`Interval time: ${intervalTime} ms`);

        const recordingsImage = document.getElementById('recordings-image');
        let frameIndex = 0;

        function displayNextFrame() {
          if (frameIndex < totalFrames) {
            console.log(`Displaying frame ${frameIndex + 1}/${totalFrames}`);
            recordingsImage.src = `data:image/jpeg;base64,${frames[frameIndex]}`;
            frameIndex++;
            setTimeout(displayNextFrame, intervalTime);
          } else {
            console.log('All frames displayed.');
          }
        }

        displayNextFrame();
      })
      .catch(error => console.error('Error playing recording:', error));
  }
  // Initial load of logs
  loadLogs();

  // Refresh logs every minute
  // setInterval(loadLogs, 60000); // Refresh every 60,000 ms (1 minute)

  function fetchDeviceInfo() {
    fetch('/device-info')
      .then(response => response.text()) // Fetch as text first
      .then(text => {
        console.log('Raw JSON response:', text); // Log the raw JSON response
        return JSON.parse(text); // Parse the JSON
      })
      .then(data => {
        document.getElementById('device-info').innerHTML = `
          <h3>Device Information</h3>
          <p><strong>Device Model:</strong> ${data.deviceModel}</p>
          <p><strong>Chip ID:</strong> ${data.chipID}</p>
          <p><strong>Chip Cores:</strong> ${data.chipCores}</p>
          <p><strong>MAC Address:</strong> ${data.MAC_Address}</p>
          <p><strong>Firmware:</strong> ${data.firmware}</p>
          <p><strong>Uptime:</strong> ${data.uptime} </p>
          <p><strong>GMT Offset:</strong> ${data.gmtoffset} seconds</p>
          <p><strong>Daylight Offset:</strong> ${data.daylightoffset} minutes</p>
          <p><strong>Interval:</strong> ${data.interval} ms</p>
          <button onclick="rebootESP()">Reboot ESP32S3</button>
        `;
        document.getElementById('wifi-status').innerHTML = `
          <p><strong>WiFi:</strong> ${data.wificonnected ? 'Connected' : 'Disconnected'}</p>
          <p><strong>Signal Strength:</strong> ${data.signalstrength} dBm</p>
          <p><strong>SSID:</strong> ${data.ssid}</p>
          <p><strong>WiFi Password:</strong> ${data.wifi_password}</p>
          <p><strong>IP Address:</strong> ${data.IP}</p>
          <p><strong>Hostname:</strong> ${data.hostname}</p>
        `;
      })
      .catch(error => console.error('Error fetching device info:', error));
  }

  function listRecordings() {
    fetch('/list-recordings') // Adjust the URL to your endpoint
      .then(response => response.json())
      .then(data => {
        const recordingsContainer = document.getElementById('recordings-list');
        recordingsContainer.innerHTML = ''; // Clear any existing content

        data.forEach(recording => {
          const recordingElement = document.createElement('div');
          recordingElement.classList.add('recording');

          const nameElement = document.createElement('p');
          nameElement.textContent = `Name: ${recording.name}`;
          recordingElement.appendChild(nameElement);

          const sizeElement = document.createElement('p');
          sizeElement.textContent = `Size: ${recording.size} bytes`;
          recordingElement.appendChild(sizeElement);

          const playButton = document.createElement('button');
          playButton.textContent = 'Play';
          // playButton.onclick = () => playRecording(recording.name);
          playButton.addEventListener('click', () => playRecording(recording.name));
          recordingElement.appendChild(playButton);

          recordingsContainer.appendChild(recordingElement);
        });
      })
      .catch(error => console.error('Error fetching recordings list:', error));
  }

  // Initialize recordings list on page load
  listRecordings();

  // Initial load of device info
  fetchDeviceInfo();
  // Refresh device info every minute (optional)
  // setInterval(fetchDeviceInfo, 60000); // Refresh every 60,000 ms (1 minute)

  // Initialize network scan and form submission
  scanNetworks();

  // Set up event listeners for network connection
  const networkList = document.getElementById('network-list');
  const manualSSID = document.getElementById('manual-ssid');
  const connectBtn = document.getElementById('connect-btn');

  if (networkList) {
    networkList.addEventListener('change', handleSelectionChange);
  } else {
    console.error('Network list not found.');
  }

  if (manualSSID) {
    manualSSID.addEventListener('input', handleSelectionChange);
  } else {
    console.error('Manual SSID input not found.');
  }

  if (connectBtn) {
    connectBtn.addEventListener('click', connectToNetwork);
  } else {
    console.error('Connect button not found.');
  }
});

unihiker_app.py

Python
import time
import json
import os
import subprocess
from unihiker import GUI

# Initialize global variables
gui=GUI()  # Instantiate the GUI class
pin = ""
unlocked = None
s1 = s2 = s3 = s4 = s5 = s6 = txt = None

def home():
    gui.clear()
    gui.fill_round_rect(x=20, y=20, w=200, h=50, r=3, color="blue", onclick=lambda: unlock())
    gui.draw_text(x=120, y=45, text='Unlock', color='white', origin='center', onclick=lambda: unlock())
    
    gui.fill_round_rect(x=20, y=90, w=200, h=50, r=3, color="red", onclick=lambda: print("fill round rect clicked"))
    gui.draw_text(x=120, y=115, text='Call', color='white', origin='center', onclick=lambda:print("fill round rect clicked"))
    
    gui.fill_round_rect(x=20, y=160, w=200, h=50, r=3, color="green", onclick=lambda: print("fill round rect clicked"))
    gui.draw_text(x=120, y=185, text='Message', color='white', origin='center', onclick=lambda:print("fill round rect clicked"))
    
    gui.fill_round_rect(x=20, y=230, w=200, h=50, r=3, color="orange", onclick=lambda: print("fill round rect clicked"))
    gui.draw_text(x=120, y=255, text='System', color='white', origin='center', onclick=lambda:print("fill round rect clicked"))
    
def pincode(data):
    global pin
    global unlocked
    if data == "clc":
        pin = ""
        s1.config(x=-30)
        s2.config(x=-60)
        s3.config(x=-90)
        s4.config(x=-120)
        s5.config(x=-150)
        s6.config(x=-180)
    else:
        unlocked = None
        pin = pin + data
        print(pin)
        lpin = len(pin)
        if lpin == 1:
            s1.config(x=30 + 15)
        elif lpin == 2:
            s2.config(x=60 + 15)
        elif lpin == 3:
            s3.config(x=90 + 15)
        elif lpin == 4:
            s4.config(x=120 + 15)
        elif lpin == 5:
            s5.config(x=150 + 15)
        elif lpin == 6:
            s6.config(x=180 + 15)
            code = check(pin)
            if code == -1:
                txt.config(text="Invalid Pin")
                unlocked = False
            else:
                txt.config(text="Unlocked: " + code['username'])
                unlocked = True
            # showpin(pin)
        elif lpin == 7:
            pin = ""
            s1.config(x=-30)
            s2.config(x=-60)
            s3.config(x=-90)
            s4.config(x=-120)
            s5.config(x=-150)
            s6.config(x=-180)
            
def check(entered_pin):
    file_path = r'/media/mmcblk0p1/postman/utilities/user.json'
    try:
        with open(file_path, 'r') as file:
            data = json.load(file)
            for entry in data:
                if entry['pincode'] == entered_pin:
                    return entry
                else:
                    return -1
    except Exception as e:
        print(f"Error reading the file: {e}")
    return -1

def set_file_permissions(file_path):
    os.chmod(file_path, 0o600)  # Set file to be readable and writable only by the owner
    
def change_pin(user_id, username, new_pin):
    file_path = r'/media/mmcblk0p1/postman/utilities/user.json'
    try:
        # Check if the file exists, if not create it with the user ID and blank fields
        if not os.path.exists(file_path):
            initial_data = [{"user_id": i, "username": "", "pincode": ""} for i in range(1, 7)]
            with open(file_path, 'w') as file:
                json.dump(initial_data, file, indent=4)
                
        set_file_permissions(file_path)
        with open(file_path, 'r') as file:
            data = json.load(file)
        
        user_found = False
        for entry in data:
            if entry['user_id'] == user_id:
                entry['username'] = username
                entry['pincode'] = new_pin
                user_found = True
                break
        
        if not user_found:
            print("User ID not found.")
            return False
        
        with open(file_path, 'w') as file:
            json.dump(data, file, indent=4)
        return True
    except Exception as e:
        print(f"Error updating the file: {e}")
        return False
        
def passcode():
    global s1, s2, s3, s4, s5, s6, txt
    gui.clear()
    gui.draw_circle(x=30 + 15, y=40, r=10, width=2, color="grey")
    gui.draw_circle(x=60 + 15, y=40, r=10, width=2, color="grey")
    gui.draw_circle(x=90 + 15, y=40, r=10, width=2, color="grey")
    gui.draw_circle(x=120 + 15, y=40, r=10, width=2, color="grey")
    gui.draw_circle(x=150 + 15, y=40, r=10, width=2, color="grey")
    gui.draw_circle(x=180 + 15, y=40, r=10, width=2, color="grey")
    
    txt = gui.draw_text(text="Enter 6-Digit Pin", x=120, y=15, font_size=15, origin="center", color="#0000FF")
    s1 = gui.fill_circle(x=-30 - 15, y=40, r=10, width=2, color="grey")
    s2 = gui.fill_circle(x=-60 - 15, y=40, r=10, width=2, color="grey")
    s3 = gui.fill_circle(x=-90 - 15, y=40, r=10, width=2, color="grey")
    s4 = gui.fill_circle(x=-120 - 15, y=40, r=10, width=2, color="grey")
    s5 = gui.fill_circle(x=-150 - 15, y=40, r=10, width=2, color="grey")
    s6 = gui.fill_circle(x=-180 - 15, y=40, r=10, width=2, color="grey")
    
    gui.add_button(x=60, y=100, w=60, h=60, text="1", origin='center', onclick=lambda: pincode("1"))
    gui.add_button(x=120, y=100, w=60, h=60, text="2", origin='center', onclick=lambda: pincode("2"))
    gui.add_button(x=180, y=100, w=60, h=60, text="3", origin='center', onclick=lambda: pincode("3"))
    gui.add_button(x=60, y=160, w=60, h=60, text="4", origin='center', onclick=lambda: pincode("4"))
    gui.add_button(x=120, y=160, w=60, h=60, text="5", origin='center', onclick=lambda: pincode("5"))
    gui.add_button(x=180, y=160, w=60, h=60, text="6", origin='center', onclick=lambda: pincode("6"))
    gui.add_button(x=60, y=220, w=60, h=60, text="7", origin='center', onclick=lambda: pincode("7"))
    gui.add_button(x=120, y=220, w=60, h=60, text="8", origin='center', onclick=lambda: pincode("8"))
    gui.add_button(x=180, y=220, w=60, h=60, text="9", origin='center', onclick=lambda: pincode("9"))
    gui.add_button(x=120, y=280, w=60, h=60, text="0", origin='center', onclick=lambda: pincode("0"))
    gui.add_button(x=180, y=280, w=60, h=60, text="Clear", origin='center', onclick=lambda: pincode("clc"))
    gui.add_button(x=60, y=280, w=60, h=60, text="Home", origin='center', onclick=lambda: home())
    
def request_open():
    gui.clear()
    gui.fill_circle(x=115, y=120, r=100, color="blue", onclick=lambda: print("Open clicked"))
    gui.draw_text(x=115, y=120, text="Open", color="white", origin="center", onclick=lambda: print("Open clicked"))
    gui.fill_round_rect(x=20, y=230, w=200, h=50, r=3, color="orange", onclick=lambda: home())
    gui.draw_text(x=120, y=255, text='Home', color='white', origin='center', onclick=lambda:home())    

def unlock():
    print('unlock')
    passcode()
    global unlocked
    time.sleep(1)
    if unlocked:
        request_open()
        unlocked = None
    elif unlocked == False:
        home()
    elif(unlocked == None):
        pass
    
def call():
    pass

def message():
    pass

def system():
    pass

def main():
    home()
    
    while True:
        time.sleep(1)
    
if __name__ == "__main__":
    main()

unihiker_uart.py

Python
from pinpong.board import UART

class Serial:
    def __init__(self, bus, baudrate, timeout=0):
        # Initialize UART with the specified parameters
        self.uart1 = UART(bus_num=bus)  # Use bus parameter for flexibility
        self.uart1.init(baud_rate=baudrate, bits=8, parity=0, stop=1, timeout=timeout)
        
    def readline(self):
        data = b''
        while self.uart1.any() > 0:
            data += self.uart1.read()
        
        if data:
            print(self.ascii_to_char(data))
        return data

    def ascii_to_char(self, ascii_values):
        return ''.join(chr(value) for value in ascii_values)

    def write(self, data):
        if isinstance(data, str):
            data = data.encode()  # Convert string to bytes if necessary
        self.uart1.write(data)

testwebcam.py

Python
import cv2
import time
# from unihiker import Audio
import os
# import subprocess
from pinpong.board import *
from pinpong.extension.unihiker import *

Board().begin()
cap = cv2.VideoCapture(0) 
# audio = Audio()  
    
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) 
fourcc = cv2.VideoWriter_fourcc(*'XVID')
out = cv2.VideoWriter('temp.avi', fourcc, 10.0, (640, 480))
start_time = time.time()
frame_count = 0
# audio.start_record('output.wav')
while(cap.isOpened()):  
    ret, img = cap.read() 
    if ret: 
        frame_count += 1
        current_time = time.time()
        elapsed_time = current_time - start_time

        if elapsed_time >= 1.0:  # Update every second
            actual_fps = frame_count / elapsed_time
            print(f"Actual capture frame rate: {actual_fps:.2f}")
            start_time = current_time
            frame_count = 0
            
        out.write(img)
        
        if button_a.is_pressed() == True: # Press the "a" key on Unihiker will stop the program.
            # audio.stop_record()
            print("exit")
            break 
    else:
        break
print("Release writer and camera")
cap.release() # Release usb camera. 
out.release() # Release video writer
cv2.destroyAllWindows() # Destory all windows created by opencv.
# cmd = "ffmpeg -y -ac 2 -channel_layout mono -i output.wav -i output.avi -pix_fmt yuv420p test.avi"
# subprocess.call(cmd, shell=True)
# os.remove('output.wav')
# os.remove('output.avi')

GitHub Project Postman

Credits

Isaiah O.

Isaiah O.

1 project • 2 followers

Comments