commit 7a985d90752167d868705b5d48df39e9f9b2897d Author: Tomislav Kopić Date: Tue Nov 26 20:50:30 2024 +0100 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6886cff --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +config.h +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..080e70d --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "platformio.platformio-ide" + ], + "unwantedRecommendations": [ + "ms-vscode.cpptools-extension-pack" + ] +} diff --git a/hardware/Case/FlashButton.stl b/hardware/Case/FlashButton.stl new file mode 100644 index 0000000..7a393c3 Binary files /dev/null and b/hardware/Case/FlashButton.stl differ diff --git a/hardware/Case/SmartCube_Back_Speaker_Side_Hole.stl b/hardware/Case/SmartCube_Back_Speaker_Side_Hole.stl new file mode 100644 index 0000000..65a8d67 Binary files /dev/null and b/hardware/Case/SmartCube_Back_Speaker_Side_Hole.stl differ diff --git a/hardware/Case/SmartCube_Body.stl b/hardware/Case/SmartCube_Body.stl new file mode 100644 index 0000000..56f5dec Binary files /dev/null and b/hardware/Case/SmartCube_Body.stl differ diff --git a/hardware/Case/SmartCube_Front.stl b/hardware/Case/SmartCube_Front.stl new file mode 100644 index 0000000..9372d5a Binary files /dev/null and b/hardware/Case/SmartCube_Front.stl differ diff --git a/hardware/Schematics/eoaza0nnhgq91.jpg b/hardware/Schematics/eoaza0nnhgq91.jpg new file mode 100644 index 0000000..6db6fac Binary files /dev/null and b/hardware/Schematics/eoaza0nnhgq91.jpg differ diff --git a/hardware/case/SmartCube_Back_Speaker_Side_Hole.stl b/hardware/case/SmartCube_Back_Speaker_Side_Hole.stl new file mode 100644 index 0000000..a4ff7f6 Binary files /dev/null and b/hardware/case/SmartCube_Back_Speaker_Side_Hole.stl differ diff --git a/hardware/case/SmartCube_Body_3_button.stl b/hardware/case/SmartCube_Body_3_button.stl new file mode 100644 index 0000000..55f4e5f Binary files /dev/null and b/hardware/case/SmartCube_Body_3_button.stl differ diff --git a/hardware/case/SmartCube_Front.stl b/hardware/case/SmartCube_Front.stl new file mode 100644 index 0000000..277a78a Binary files /dev/null and b/hardware/case/SmartCube_Front.stl differ diff --git a/hardware/schematics/esp8266.png b/hardware/schematics/esp8266.png new file mode 100644 index 0000000..5d9da34 Binary files /dev/null and b/hardware/schematics/esp8266.png differ diff --git a/include/README b/include/README new file mode 100644 index 0000000..194dcd4 --- /dev/null +++ b/include/README @@ -0,0 +1,39 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the usual convention is to give header files names that end with `.h'. +It is most portable to use only letters, digits, dashes, and underscores in +header file names, and at most one dot. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/lib/README b/lib/README new file mode 100644 index 0000000..2593a33 --- /dev/null +++ b/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into executable file. + +The source code of each library should be placed in an own separate directory +("lib/your_library_name/[here are source files]"). + +For example, see a structure of the following two libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +and a contents of `src/main.c`: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +PlatformIO Library Dependency Finder will find automatically dependent +libraries scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..c062ae8 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,19 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env:d1_mini_lite] +platform = espressif8266 +board = d1_mini_lite +framework = arduino +; board_build.f_cpu = 160000000L ; uncomment to set CPU frequency to 160 MHz +lib_deps = + bblanchon/ArduinoJson@^7.2.0 + adafruit/Adafruit SSD1306@^2.5.13 + arduino-libraries/NTPClient@^3.2.1 \ No newline at end of file diff --git a/src/SmartCube/cubeBattery.h b/src/SmartCube/cubeBattery.h new file mode 100644 index 0000000..c9ea52b --- /dev/null +++ b/src/SmartCube/cubeBattery.h @@ -0,0 +1,18 @@ +#define ADC_PIN 10 // GPIO10 on WEMOS S2 Mini + +const float R1 = 100000.0; // 100k ohms +const float R2 = 47000.0; // 47k ohms +const float ADC_MAX = 4095.0; // 12-bit ADC resolution +const float V_REF = 3.3; // Reference voltage for ESP32-S2 ADC + +float readBatteryVoltage() { + int rawADC = analogRead(ADC_PIN); + float voltage = (rawADC / ADC_MAX) * V_REF; + return voltage * (R1 + R2) / R2; // Scale to actual battery voltage +} + +int batteryPercentage(float voltage) { + if (voltage >= 4.2) return 100; + if (voltage <= 3.0) return 0; + return (voltage - 3.0) * 100 / (4.2 - 3.0); // Linear mapping +} \ No newline at end of file diff --git a/src/SmartCube/cubeButtons.h b/src/SmartCube/cubeButtons.h new file mode 100644 index 0000000..5eecf0f --- /dev/null +++ b/src/SmartCube/cubeButtons.h @@ -0,0 +1,105 @@ +extern Adafruit_SSD1306 display; // Reference to the OLED display object +bool is_display_off = false; // State variable to track display on/off status + +int buttonDebounceDelay = 50; // Debounce delay in milliseconds +int buttonLongPressDelay = 2000; // Long press threshold in milliseconds + +void cubeButtonHandler() { + unsigned long currentMillis = millis(); // Get the current time in milliseconds + + // Track the start times of button presses + static unsigned long leftPressStart = 0; + static unsigned long middlePressStart = 0; + static unsigned long rightPressStart = 0; + + // Track if a beep has already been played for each button to avoid repeated beeping + static bool leftBeeped = false; + static bool middleBeeped = false; + static bool rightBeeped = false; + + // Check if each button is currently pressed + bool leftPressed = (digitalRead(PIN_BTN_L) == HIGH); + bool middlePressed = (digitalRead(PIN_BTN_M) == HIGH); + bool rightPressed = (digitalRead(PIN_BTN_R) == HIGH); + + // Short press detection for left button + if (leftPressed) { + // Check if the button is held beyond the debounce delay + if ((currentMillis - leftPressStart > buttonDebounceDelay)) { + if (!leftBeeped) { + beep(1000); // Play beep for a short press + leftBeeped = true; // Mark as beeped to avoid repeat beeps + // Handle left short press action here + } + } + } else { + leftPressStart = currentMillis; // Reset timer when button is released + leftBeeped = false; // Reset beep flag for next press + } + + // Short press detection for middle button + if (middlePressed) { + if ((currentMillis - middlePressStart > buttonDebounceDelay)) { + if (!middleBeeped) { + beep(1000); // Play beep for a short press + middleBeeped = true; // Mark as beeped to avoid repeat beeps + // Handle middle short press action here + } + } + } else { + middlePressStart = currentMillis; // Reset timer when button is released + middleBeeped = false; // Reset beep flag for next press + } + + // Short press detection for right button + if (rightPressed) { + if ((currentMillis - rightPressStart > buttonDebounceDelay)) { + if (!rightBeeped) { + beep(1000); // Play beep for a short press + rightBeeped = true; // Mark as beeped to avoid repeat beeps + if (is_display_off) { + display.ssd1306_command(SSD1306_DISPLAYON); // Turn on the display + is_display_off = false; // Update display state + beep(1300); // Additional beep to confirm display is on + } + } + } + } else { + rightPressStart = currentMillis; // Reset timer when button is released + rightBeeped = false; // Reset beep flag for next press + } + + // Long press detection for left button + if (leftPressed && (currentMillis - leftPressStart > buttonLongPressDelay)) { + if (!leftBeeped) { + beep(1000); // Play beep for a long press + leftBeeped = true; // Mark as beeped to avoid repeat beeps + // Handle left long press action here + } + } + + // Long press detection for middle button + if (middlePressed && (currentMillis - middlePressStart > buttonLongPressDelay)) { + if (!middleBeeped) { + beep(1000); // Play beep for a long press + middleBeeped = true; // Mark as beeped to avoid repeat beeps + // Handle middle long press action here + } + } + + // Long press detection for right button (with display control) + if (rightPressed && (currentMillis - rightPressStart > buttonLongPressDelay)) { + if (!is_display_off) { // Turn off the display if it's on + beep(1300); // Beep to indicate display turn-off + display.ssd1306_command(SSD1306_DISPLAYOFF); // Turn off the display + is_display_off = true; // Update display state + beep(1000); // Additional beep to confirm display is off + } + } + + // Combined long press detection for left and middle buttons to restart device + if (leftPressed && middlePressed && + (currentMillis - leftPressStart > buttonLongPressDelay) && (currentMillis - middlePressStart > buttonLongPressDelay)) { + ESP.restart(); // Restart device if both buttons are held long enough + } +} diff --git a/src/SmartCube/cubeSound.h b/src/SmartCube/cubeSound.h new file mode 100644 index 0000000..a7f559c --- /dev/null +++ b/src/SmartCube/cubeSound.h @@ -0,0 +1,5 @@ +void beep(int buzz) { + tone(PIN_BUZZER, buzz, 100); // Generate a tone on the buzzer at the specified frequency for 100 ms + delay(100); // Wait for the tone to finish playing + noTone(PIN_BUZZER); // Stop the tone on the buzzer +} \ No newline at end of file diff --git a/src/SmartCube/cubeWifiManager.h b/src/SmartCube/cubeWifiManager.h new file mode 100644 index 0000000..0e9b836 --- /dev/null +++ b/src/SmartCube/cubeWifiManager.h @@ -0,0 +1,336 @@ +#include +#include +#include +#include +#include +#include + +// Constants for the HTML pages and config file +static const String beginHtml = "SmartCube Configure

Configure AP


"; +static const String endHtml = "
"; +static const String configFile = "/cubeWifiManager"; + +// Timeout for WiFi connection attempts +static const unsigned long connectionTimeout = 15000; // 15 seconds +class cubeWifiManager { +public: + // Constructors + cubeWifiManager(Adafruit_SSD1306& display); + cubeWifiManager(String ssid, String pass, bool hidden, Adafruit_SSD1306& display); + + // Public methods + bool start(); + void reset(); + void addSsid(String ssid, String password); + void removeSsid(String ssid, String password); + +private: + // Private members + Adafruit_SSD1306& display; + std::unique_ptr server; + std::map _ssids; + String _ssid, _pass; + bool _hidden; + + // Private methods + void init(String ssid, String pass, bool hidden); + bool tryConnectToSsid(const char* ssid, const char* pass); + bool tryConnect(); + void createAP(); + bool redirectToIp(); + void readConfig(); + void writeConfig(); + void handleRoot(); + void handleAdd(); + void handleRemove(); + void handleSelect(); +}; + +// Initialization +void cubeWifiManager::init(String ssid, String pass, bool hidden) { + // Ensure password meets minimum length + if (pass != "" && pass.length() < 8) { + display.clearDisplay(); + display.setCursor(0, 0); + display.println("Password too short"); + display.display(); + pass = "8characters"; + } + + // Get the last 4 characters of the MAC address + String macAddress = WiFi.macAddress(); + String macSuffix = macAddress.substring(macAddress.length() - 5); + macSuffix.replace(":", ""); + + // Set default SSID if none provided + _ssid = ssid.isEmpty() ? "SmartCube_" + macSuffix : ssid; + _pass = pass; + _hidden = hidden; + + + // Initialize LittleFS with error handling + if (!LittleFS.begin()) { + display.clearDisplay(); + display.setCursor(0, 0); + display.println("FS init failed"); + display.display(); + } +} + +// Constructors +cubeWifiManager::cubeWifiManager(Adafruit_SSD1306& display) : display(display) { + init("", "", false); +} + +cubeWifiManager::cubeWifiManager(String ssid, String pass, bool hidden, Adafruit_SSD1306& display) : display(display) { + init(ssid, pass, hidden); +} + +// Attempt to start and connect or create AP +bool cubeWifiManager::start() { + if (_pass == "8characters") { + display.clearDisplay(); + display.setCursor(0, 0); + display.println("Using default pass:"); + display.println("8characters"); + display.display(); + } + return tryConnect() || (createAP(), false); +} + +// Attempt connection to each saved SSID in order +bool cubeWifiManager::tryConnect() { + readConfig(); + for (auto const& item : _ssids) { + if (tryConnectToSsid(item.first.c_str(), item.second.c_str())) { + return true; + } + } + return false; +} + +// Attempt to connect to a specific SSID with timeout, dot animation, and IP display +bool cubeWifiManager::tryConnectToSsid(const char* ssid, const char* pass) { + WiFi.begin(ssid, pass); + unsigned long start = millis(); + + // Clear display and set initial message + display.clearDisplay(); + display.setCursor(0, 0); + display.println("Connecting to WiFi:"); + display.setCursor(0, 11); + display.println(String(ssid)); + display.display(); + + int dotCount = 0; + while (millis() - start < connectionTimeout) { + delay(500); + + // Check WiFi connection status + if (WiFi.status() == WL_CONNECTED) { + // Success message with IP address + display.clearDisplay(); + display.setCursor(0, 0); + display.println("Connected!"); + display.setCursor(0, 12); + display.print("IP: "); + display.println(WiFi.localIP()); + display.display(); + delay(500); + return true; + } + + // Animate by adding dots + display.setCursor(0, 20); + for (int i = 0; i < dotCount; i++) { + display.print("."); + } + display.display(); + dotCount = (dotCount + 1); + } + + // Connection failed + display.clearDisplay(); + display.setCursor(0, 0); + display.println("Connection failed."); + display.display(); + WiFi.disconnect(); + return false; +} + +// Setup Access Point with DNS and HTTP server +void cubeWifiManager::createAP() { + WiFi.softAPdisconnect(true); + WiFi.mode(WIFI_AP); + WiFi.softAP(_ssid.c_str(), _pass.c_str(), 1, _hidden); + + display.clearDisplay(); + display.setCursor(0, 0); + display.println("AccessPoint created"); + display.setCursor(0, 14); + display.println("SSID:"); + display.setCursor(0, 24); + display.setTextSize(1); + display.println(_ssid.c_str()); + display.setTextSize(1); + display.setCursor(0, 40); + display.println("Config portal:"); + display.setCursor(0, 50); + display.print("http://"); + display.println(WiFi.softAPIP().toString()); + display.display(); + + server.reset(new ESP8266WebServer(80)); + DNSServer dnsServer; + dnsServer.start(53, "*", WiFi.softAPIP()); + + server->on("/", std::bind(&cubeWifiManager::handleRoot, this)); + server->on("/add", std::bind(&cubeWifiManager::handleAdd, this)); + server->on("/remove", std::bind(&cubeWifiManager::handleRemove, this)); + server->on("/select", std::bind(&cubeWifiManager::handleSelect, this)); + + server->begin(); + + while (true) { + dnsServer.processNextRequest(); + server->handleClient(); + delay(10); + } +} + +// Redirect to AP IP if not accessed directly +bool cubeWifiManager::redirectToIp() { + if (server->hostHeader() == WiFi.softAPIP().toString()) { + return false; + } + server->sendHeader("Location", "http://" + WiFi.softAPIP().toString(), true); + server->send(302, "text/plain", ""); + server->client().stop(); + return true; +} + +// Modify the addSsid function to take parameters from the `select` page +void cubeWifiManager::addSsid(String ssid, String password) { + _ssids[ssid] = password; + writeConfig(); + + // Attempt to connect to the selected network + if (tryConnectToSsid(ssid.c_str(), password.c_str())) { + // Redirect to the main page on success + server->sendHeader("Location", "/", true); + server->send(302, "text/plain", ""); + } else { + // Show error message if connection failed + server->send(200, "text/html", "

Connection failed. Please try again.

"); + } +} + +// Remove SSID from config +void cubeWifiManager::removeSsid(String ssid, String password) { + if (_ssids.count(ssid) && _ssids[ssid] == password) { + _ssids.erase(ssid); + writeConfig(); + } +} + +// Handle file-based config loading +void cubeWifiManager::readConfig() { + _ssids.clear(); + File file = LittleFS.open(configFile, "r"); + if (!file) { + display.clearDisplay(); + display.setCursor(0, 0); + display.println("Config not found"); + display.display(); + return; + } + + while (file.available()) { + String ssid = file.readStringUntil('\n'); + ssid.trim(); + String pass = file.readStringUntil('\n'); + pass.trim(); + _ssids[ssid] = pass; + } + file.close(); +} + +// Handle file-based config saving +void cubeWifiManager::writeConfig() { + File file = LittleFS.open(configFile, "w"); + for (const auto& item : _ssids) { + file.println(item.first); + file.println(item.second); + } + file.close(); +} + +// Reset configuration +void cubeWifiManager::reset() { + LittleFS.remove(configFile); + _ssids.clear(); +} + +String urlEncode(const String &str) { + String encoded = ""; + char c; + for (size_t i = 0; i < str.length(); i++) { + c = str.charAt(i); + if (isalnum(c)) { + encoded += c; + } else { + encoded += '%'; + char buf[3]; + snprintf(buf, sizeof(buf), "%02X", c); + encoded += buf; + } + } + return encoded; +} + +void cubeWifiManager::handleRoot() { + if (redirectToIp()) return; + + // Scan for available networks + int n = WiFi.scanNetworks(); + String result = beginHtml; + + // Add stored SSIDs to the page + result += "

Saved Networks

"; + for (const auto& item : _ssids) { + result += "" + item.first + "-" + item.second + ""; + } + + // Display available WiFi networks + result += "

Available Networks

"; + for (int i = 0; i < n; i++) { + // Get SSID and signal strength + String ssid = WiFi.SSID(i); + int rssi = WiFi.RSSI(i); + bool openNetwork = (WiFi.encryptionType(i) == ENC_TYPE_NONE); + + // Show network with button to select + result += ""; + } + result += endHtml; + server->send(200, "text/html", result); +} + +void cubeWifiManager::handleAdd() { + server->send(200, "text/html", "The ESP will now reboot."); + addSsid(server->arg("ssid"), server->arg("pass")); + delay(500); + ESP.restart(); +} + +void cubeWifiManager::handleRemove() { + removeSsid(server->arg("ssid"), server->arg("pass")); + handleRoot(); +} + +// Add SSID to config and save +void cubeWifiManager::handleSelect() { + String ssid = server->arg("ssid"); + String selectPage = "Connect to " + ssid + "

Connect to " + ssid + "

"; + server->send(200, "text/html", selectPage); +} \ No newline at end of file diff --git a/src/example_config.h b/src/example_config.h new file mode 100644 index 0000000..e5382df --- /dev/null +++ b/src/example_config.h @@ -0,0 +1,15 @@ +// Zabbix API details +const char* zabbixServer = "http://your.zabbix.com/api_jsonrpc.php"; +const char* zabbixToken = "Pu770k3nH3r3"; + +// OLED display settings +#define SCREEN_WIDTH 128 +#define SCREEN_HEIGHT 64 +#define OLED_RESET -1 +#define FRAMERATE 8 + +// Buttons and buzzer +#define PIN_BTN_L 12 // D6 +#define PIN_BTN_M 13 // D7 +#define PIN_BTN_R 15 // D8 +#define PIN_BUZZER 0 // D3 \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..a9faacb --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,189 @@ +#include +#include +#include +#include +#include "example_config.h" +#include "SmartCube/cubeSound.h" +#include "SmartCube/cubeButtons.h" +#include "SmartCube/cubeWifiManager.h" + +Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); +cubeWifiManager cubeWifiManager(display); + +unsigned long lastRefresh = 0; +const unsigned long refreshInterval = 60000; // 60 seconds +unsigned long lastDisplayUpdate = 0; +const unsigned long displayInterval = 100; // 100 ms + +String lastEventId = ""; +String problems[10]; +int problemCount = 0; +int severityCounts[5] = {0}; +bool newProblemsDetected = false; + +void initSystems() { + pinMode(PIN_BTN_L, INPUT); + pinMode(PIN_BTN_M, INPUT); + pinMode(PIN_BTN_R, INPUT); + pinMode(PIN_BUZZER, OUTPUT); + pinMode(LED_BUILTIN, OUTPUT); + digitalWrite(LED_BUILTIN, HIGH); + Wire.begin(); // Initialize I2C + Wire.setClock(400000L); // Set I2C to Fast Mode (400 kHz) + display.ssd1306_command(SSD1306_SETCONTRAST); + display.ssd1306_command(200); // Value between 0 and 255 + // Initialize the SSD1306 display + if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Adjust I2C address if needed + for(;;); // Don't proceed, loop forever + } + + // Rotate the display 180 degrees + display.setRotation(2); + + // Clear the display buffer + display.clearDisplay(); + display.setTextSize(1); + display.setTextColor(WHITE); + display.setCursor(0, 0); + display.display(); +} + +void fetchActiveProblems() { + WiFiClient client; + HTTPClient http; + http.begin(client, zabbixServer); + http.addHeader("Content-Type", "application/json"); + + // JSON payload + DynamicJsonDocument doc(512); + doc["jsonrpc"] = "2.0"; + doc["method"] = "problem.get"; + doc["id"] = 1; + doc["auth"] = zabbixToken; + JsonObject params = doc["params"].to(); + params["output"] = "extend"; + params["recent"] = true; + params["sortfield"] = "eventid"; + params["sortorder"] = "DESC"; + + String requestBody; + serializeJson(doc, requestBody); + + // POST request + int httpResponseCode = http.POST(requestBody); + + if (httpResponseCode > 0) { + String response = http.getString(); + + // Parse response + DynamicJsonDocument responseDoc(4096); + deserializeJson(responseDoc, response); + + JsonArray result = responseDoc["result"].as(); + int newProblemCount = 0; + memset(severityCounts, 0, sizeof(severityCounts)); // Reset severities + + for (JsonObject problem : result) { + String eventId = problem["eventid"].as(); + int severity = problem["severity"].as(); + String description = problem["name"].as(); + + // Check for new problems by comparing event IDs as integers + if (eventId.toInt() > lastEventId.toInt()) { + newProblemsDetected = true; + } + + // Add new problem descriptions to the array + if (newProblemCount < 10) { + problems[newProblemCount] = description; + } + + // Count severities + if (severity >= 0 && severity < 5) { + severityCounts[severity]++; + } + + newProblemCount++; + } + + problemCount = newProblemCount; + + // Update lastEventId after processing the most recent event + if (problemCount > 0) { + lastEventId = result[0]["eventid"].as(); + } + } else { + beep(2000); + } + + http.end(); +} + +void displayProblems() { + static int scrollPos = SCREEN_WIDTH; + static unsigned long lastScrollTime = 0; + + if (millis() - lastDisplayUpdate < displayInterval) { + return; // Skip rendering if the display interval hasn't elapsed + } + lastDisplayUpdate = millis(); + + display.clearDisplay(); + + // Header + display.setTextSize(1); + display.setCursor(0, 0); + display.print("Problems: "); + display.print(problemCount); + + // Display severity counts (S0 to S4) + for (int i = 0; i < 5; i++) { + display.setCursor(0, 10 + (i * 10)); + display.printf("S%d: %d", i, severityCounts[i]); + } + + // Scrolling description of the first problem + if (problemCount > 0) { + String problemText = problems[0]; + int16_t x1, y1; + uint16_t textWidth, textHeight; + display.getTextBounds(problemText, 0, 55, &x1, &y1, &textWidth, &textHeight); + + if (millis() - lastScrollTime > 100) { + scrollPos -= 2; + if (scrollPos < -textWidth) { + scrollPos = SCREEN_WIDTH; + } + lastScrollTime = millis(); + } + + display.setTextSize(1); + display.setCursor(scrollPos, 55); + display.print(problemText); + } + + display.display(); +} + +void setup() { + initSystems(); + cubeWifiManager.start(); + fetchActiveProblems(); +} + +void loop() { + unsigned long currentMillis = millis(); + cubeButtonHandler(); + + if (currentMillis - lastRefresh >= refreshInterval) { + lastRefresh = currentMillis; + fetchActiveProblems(); + } + + displayProblems(); + + if (newProblemsDetected) { + beep(1000); + newProblemsDetected = false; + } +} \ No newline at end of file diff --git a/src/netman.h b/src/netman.h new file mode 100644 index 0000000..d8c131a --- /dev/null +++ b/src/netman.h @@ -0,0 +1,333 @@ +#include +#include +#include +#include +#include +#include +#include + +// Constants for the HTML pages and config file +static const String beginHtml = "SmartCube Configure

Configure AP

" + (openNetwork ? "(Open)" : "(Secured)") + "" + String(rssi) + " dBm

"; +static const String endHtml = "
"; +static const String configFile = "/netman"; + +// Timeout for WiFi connection attempts +static const unsigned long connectionTimeout = 15000; // 15 seconds + +struct netman { +public: + netman(Adafruit_SSD1306& display); + netman(String ssid, String pass, bool hidden, Adafruit_SSD1306& display); + bool start(); + void reset(); + void addSsid(String ssid, String password); + void removeSsid(String ssid, String password); + +private: + Adafruit_SSD1306& display; + std::unique_ptr server; + std::map _ssids; + String _ssid, _pass; + bool _hidden; + + void init(String ssid, String pass, bool hidden); + bool tryConnectToSsid(const char* ssid, const char* pass); + bool tryConnect(); + void createAP(); + bool redirectToIp(); + void readConfig(); + void writeConfig(); + void handleRoot(); + void handleAdd(); + void handleRemove(); + void handleSelect(); +}; + +// Initialization +void netman::init(String ssid, String pass, bool hidden) { + // Ensure password meets minimum length + if (pass != "" && pass.length() < 8) { + display.clearDisplay(); + display.setCursor(0, 0); + display.println("Password too short"); + display.display(); + pass = "8characters"; + } + + // Get the last 4 characters of the MAC address + String macAddress = WiFi.macAddress(); + String macSuffix = macAddress.substring(macAddress.length() - 5); + macSuffix.replace(":", ""); + + // Set default SSID if none provided + _ssid = ssid.isEmpty() ? "SmartCube_" + macSuffix : ssid; + _pass = pass; + _hidden = hidden; + + + // Initialize LittleFS with error handling + if (!LittleFS.begin()) { + display.clearDisplay(); + display.setCursor(0, 0); + display.println("FS init failed"); + display.display(); + } +} + +// Constructors +netman::netman(Adafruit_SSD1306& display) : display(display) { + init("", "", false); +} + +netman::netman(String ssid, String pass, bool hidden, Adafruit_SSD1306& display) : display(display) { + init(ssid, pass, hidden); +} + +// Attempt to start and connect or create AP +bool netman::start() { + if (_pass == "8characters") { + display.clearDisplay(); + display.setCursor(0, 0); + display.println("Using default pass:"); + display.println("8characters"); + display.display(); + } + return tryConnect() || (createAP(), false); +} + +// Attempt connection to each saved SSID in order +bool netman::tryConnect() { + readConfig(); + for (auto const& item : _ssids) { + if (tryConnectToSsid(item.first.c_str(), item.second.c_str())) { + return true; + } + } + return false; +} + +// Attempt to connect to a specific SSID with timeout, dot animation, and IP display +bool netman::tryConnectToSsid(const char* ssid, const char* pass) { + WiFi.begin(ssid, pass); + unsigned long start = millis(); + + // Clear display and set initial message + display.clearDisplay(); + display.setCursor(0, 0); + display.println("Connecting to WiFi:"); + display.setCursor(0, 11); + display.println(String(ssid)); + display.display(); + + int dotCount = 0; + while (millis() - start < connectionTimeout) { + delay(500); + + // Check WiFi connection status + if (WiFi.status() == WL_CONNECTED) { + // Success message with IP address + display.clearDisplay(); + display.setCursor(0, 0); + display.println("Connected!"); + display.setCursor(0, 12); + display.print("IP: "); + display.println(WiFi.localIP()); + display.display(); + delay(500); + return true; + } + + // Animate by adding dots + display.setCursor(0, 20); + for (int i = 0; i < dotCount; i++) { + display.print("."); + } + display.display(); + dotCount = (dotCount + 1); + } + + // Connection failed + display.clearDisplay(); + display.setCursor(0, 0); + display.println("Connection failed."); + display.display(); + WiFi.disconnect(); + return false; +} + +// Setup Access Point with DNS and HTTP server +void netman::createAP() { + WiFi.softAPdisconnect(true); + WiFi.mode(WIFI_AP); + WiFi.softAP(_ssid.c_str(), _pass.c_str(), 1, _hidden); + + display.clearDisplay(); + display.setCursor(0, 0); + display.println("AccessPoint created"); + display.setCursor(0, 14); + display.println("SSID:"); + display.setCursor(0, 24); + display.setTextSize(1); + display.println(_ssid.c_str()); + display.setTextSize(1); + display.setCursor(0, 40); + display.println("Config portal:"); + display.setCursor(0, 50); + display.print("http://"); + display.println(WiFi.softAPIP().toString()); + display.display(); + + server.reset(new ESP8266WebServer(80)); + DNSServer dnsServer; + dnsServer.start(53, "*", WiFi.softAPIP()); + + server->on("/", std::bind(&netman::handleRoot, this)); + server->on("/add", std::bind(&netman::handleAdd, this)); + server->on("/remove", std::bind(&netman::handleRemove, this)); + server->on("/select", std::bind(&netman::handleSelect, this)); + + server->begin(); + + while (true) { + dnsServer.processNextRequest(); + server->handleClient(); + delay(10); + } +} + +// Redirect to AP IP if not accessed directly +bool netman::redirectToIp() { + if (server->hostHeader() == WiFi.softAPIP().toString()) { + return false; + } + server->sendHeader("Location", "http://" + WiFi.softAPIP().toString(), true); + server->send(302, "text/plain", ""); + server->client().stop(); + return true; +} + +// Modify the addSsid function to take parameters from the `select` page +void netman::addSsid(String ssid, String password) { + _ssids[ssid] = password; + writeConfig(); + + // Attempt to connect to the selected network + if (tryConnectToSsid(ssid.c_str(), password.c_str())) { + // Redirect to the main page on success + server->sendHeader("Location", "/", true); + server->send(302, "text/plain", ""); + } else { + // Show error message if connection failed + server->send(200, "text/html", "

Connection failed. Please try again.

"); + } +} + +// Remove SSID from config +void netman::removeSsid(String ssid, String password) { + if (_ssids.count(ssid) && _ssids[ssid] == password) { + _ssids.erase(ssid); + writeConfig(); + } +} + +// Handle file-based config loading +void netman::readConfig() { + _ssids.clear(); + File file = LittleFS.open(configFile, "r"); + if (!file) { + display.clearDisplay(); + display.setCursor(0, 0); + display.println("Config not found"); + display.display(); + return; + } + + while (file.available()) { + String ssid = file.readStringUntil('\n'); + ssid.trim(); + String pass = file.readStringUntil('\n'); + pass.trim(); + _ssids[ssid] = pass; + } + file.close(); +} + +// Handle file-based config saving +void netman::writeConfig() { + File file = LittleFS.open(configFile, "w"); + for (const auto& item : _ssids) { + file.println(item.first); + file.println(item.second); + } + file.close(); +} + +// Reset configuration +void netman::reset() { + LittleFS.remove(configFile); + _ssids.clear(); +} + +String urlEncode(const String &str) { + String encoded = ""; + char c; + for (size_t i = 0; i < str.length(); i++) { + c = str.charAt(i); + if (isalnum(c)) { + encoded += c; + } else { + encoded += '%'; + char buf[3]; + snprintf(buf, sizeof(buf), "%02X", c); + encoded += buf; + } + } + return encoded; +} + +void netman::handleRoot() { + if (redirectToIp()) return; + + // Scan for available networks + int n = WiFi.scanNetworks(); + String result = beginHtml; + + // Add stored SSIDs to the page + result += "

Saved Networks

"; + for (const auto& item : _ssids) { + result += "" + item.first + "-" + item.second + ""; + } + + // Display available WiFi networks + result += "

Available Networks

"; + for (int i = 0; i < n; i++) { + // Get SSID and signal strength + String ssid = WiFi.SSID(i); + int rssi = WiFi.RSSI(i); + bool openNetwork = (WiFi.encryptionType(i) == ENC_TYPE_NONE); + + // Show network with button to select + result += ""; + } + result += endHtml; + server->send(200, "text/html", result); +} + +void netman::handleAdd() { + server->send(200, "text/html", "The ESP will now reboot."); + addSsid(server->arg("ssid"), server->arg("pass")); + delay(500); + ESP.restart(); +} + +void netman::handleRemove() { + removeSsid(server->arg("ssid"), server->arg("pass")); + handleRoot(); +} + +// Add SSID to config and save +void netman::handleSelect() { + String ssid = server->arg("ssid"); + String selectPage = "Connect to " + ssid + "

Connect to " + ssid + "

"; + server->send(200, "text/html", selectPage); +} \ No newline at end of file diff --git a/test/README b/test/README new file mode 100644 index 0000000..9b1e87b --- /dev/null +++ b/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Test Runner and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html
" + (openNetwork ? "(Open)" : "(Secured)") + "" + String(rssi) + " dBm