Hardware components | ||||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
Software apps and online services | ||||||
| ||||||
| ||||||
Hand tools and fabrication machines | ||||||
| ||||||
|
Project Postman is an IoT solution focused on monitoring and securing home deliveries, with three key goals: accessibility, user experience, and security.
This project was supported in parts by the Build2gether 2.0 Inclusive Innovation Challenge. The project is not fully complete and the code provide will serve a demo for some of curretnly implemented features.The Problem
With the rise of home deliveries from retail and medical providers, individuals with mobility impairments face significant challenges in retrieving packages left on porches or doorsteps. Porch piracy is a growing issue, and for those unable to quickly collect their deliveries, packages are often left unattended, increasing the risk of theft. While services like Amazon's secure pick-up locations exist, they are not accessible to everyone, especially those with limited mobility, and only cater to select retailers, leaving critical deliveries like medications vulnerable.
The SolutionProject Postman developed to be an IoT-powered package chest designed to assist individuals with mobility impairments. The chest locks and unlocks to securely store deliveries, with remote monitoring and access through a web interface. Using an XIAO ESP32-S3 to host an async web server, the device can be controlled via any browser, enabling users to manage lock status, view live streams, and receive alerts. The Unhiker operates as an on-device interface, allowing someone one to request to unlock or unlock with a pin code. There are future plans to integrate the nRF52840 DK for voice commands and smart home compatibility, to provide a more accessible and another secure way for users to manage deliveries.
Proposed SystemThe system is compised of three main boards ESP32, Unihker, nRF52 each boad is responsible for different task to increase accessibility of the device.
XIAO ESP32-S3 Sense (ESP32)
The ESP32 host a web app on an asynchronous web server.
Unihiker
#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.
<!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>
: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;
}
<!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>
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.');
}
});
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()
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)
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')
Comments