first commit
This commit is contained in:
		
							
								
								
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | config.h | ||||||
|  | .pio | ||||||
|  | .vscode/.browse.c_cpp.db* | ||||||
|  | .vscode/c_cpp_properties.json | ||||||
|  | .vscode/launch.json | ||||||
|  | .vscode/ipch | ||||||
							
								
								
									
										10
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -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" | ||||||
|  |     ] | ||||||
|  | } | ||||||
							
								
								
									
										
											BIN
										
									
								
								hardware/Case/FlashButton.stl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								hardware/Case/FlashButton.stl
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								hardware/Case/SmartCube_Back_Speaker_Side_Hole.stl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								hardware/Case/SmartCube_Back_Speaker_Side_Hole.stl
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								hardware/Case/SmartCube_Body.stl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								hardware/Case/SmartCube_Body.stl
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								hardware/Case/SmartCube_Front.stl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								hardware/Case/SmartCube_Front.stl
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								hardware/Schematics/eoaza0nnhgq91.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								hardware/Schematics/eoaza0nnhgq91.jpg
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 5.4 MiB | 
							
								
								
									
										
											BIN
										
									
								
								hardware/case/SmartCube_Back_Speaker_Side_Hole.stl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								hardware/case/SmartCube_Back_Speaker_Side_Hole.stl
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								hardware/case/SmartCube_Body_3_button.stl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								hardware/case/SmartCube_Body_3_button.stl
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								hardware/case/SmartCube_Front.stl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								hardware/case/SmartCube_Front.stl
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								hardware/schematics/esp8266.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								hardware/schematics/esp8266.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 619 KiB | 
							
								
								
									
										39
									
								
								include/README
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								include/README
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||||
							
								
								
									
										46
									
								
								lib/README
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								lib/README
									
									
									
									
									
										Normal file
									
								
							| @@ -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 <Foo.h> | ||||||
|  | #include <Bar.h> | ||||||
|  |  | ||||||
|  | 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 | ||||||
							
								
								
									
										19
									
								
								platformio.ini
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								platformio.ini
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||||
							
								
								
									
										18
									
								
								src/SmartCube/cubeBattery.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/SmartCube/cubeBattery.h
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||||
|  | } | ||||||
							
								
								
									
										105
									
								
								src/SmartCube/cubeButtons.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								src/SmartCube/cubeButtons.h
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										5
									
								
								src/SmartCube/cubeSound.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/SmartCube/cubeSound.h
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||||
|  | } | ||||||
							
								
								
									
										336
									
								
								src/SmartCube/cubeWifiManager.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										336
									
								
								src/SmartCube/cubeWifiManager.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,336 @@ | |||||||
|  | #include <ESP8266WiFi.h> | ||||||
|  | #include <WiFiClient.h> | ||||||
|  | #include <ESP8266WebServer.h> | ||||||
|  | #include <LittleFS.h> | ||||||
|  | #include <DNSServer.h> | ||||||
|  | #include <map> | ||||||
|  |  | ||||||
|  | // Constants for the HTML pages and config file | ||||||
|  | static const String beginHtml = "<!DOCTYPE html><html lang='en'><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width, initial-scale=1.0'><title>SmartCube Configure</title><style>body{font-family:Arial,sans-serif;background-color:#f4f4f9;color:#333;display:flex;align-items:center;justify-content:center;height:100vh;margin:0}.container{width:100%;max-width:300px;padding:20px;background:#fff;border-radius:10px;box-shadow:0 4px 8px rgba(0,0,0,0.2);box-sizing:border-box}h2{margin-top:0;text-align:center;color:#0073e6}label{display:block;margin-bottom:5px;font-weight:bold}input[type='text'],input[type='password']{width:100%;padding:8px;margin-bottom:15px;border:1px solid #ccc;border-radius:4px;box-sizing:border-box}button{width:100%;padding:10px;background-color:#0073e6;color:white;border:none;border-radius:4px;cursor:pointer;font-size:16px}button:hover{background-color:#005bb5}table{width:100%;margin-top:20px}td{padding:5px;text-align:left}</style></head><body><div class='container'><h2>Configure AP</h2><table><tbody><tr><td><label for='ssid'>SSID</label></td><td><input id='ssid' type='text' placeholder='Enter SSID'/></td></tr><tr><td><label for='pass'>Password</label></td><td><input id='pass' type='password' placeholder='Enter Password'/></td></tr><tr><td colspan='2'><button onclick=\"location.href = '/add?ssid=' + encodeURIComponent(document.getElementById('ssid').value) + '&pass=' + encodeURIComponent(document.getElementById('pass').value);\">Add Network</button></td></tr></tbody></table><br/><table><tbody>"; | ||||||
|  | static const String endHtml = "</tbody></table></body></html>"; | ||||||
|  | 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<ESP8266WebServer> server; | ||||||
|  |     std::map<String, String> _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", "<html><body><h2>Connection failed. Please try again.</h2></body></html>"); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 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 += "<h3>Saved Networks</h3>"; | ||||||
|  |     for (const auto& item : _ssids) { | ||||||
|  |         result += "<tr><td><button onclick=\"location.href='/remove?ssid=' + escape('" + item.first + "') + '&pass=' + escape('" + item.second + "') \">×</button></td><td>" + item.first + "</td><td>-</td><td>" + item.second + "</td></tr>"; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Display available WiFi networks | ||||||
|  |     result += "</tbody></table><h3>Available Networks</h3><table><tbody>"; | ||||||
|  |     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 += "<tr><td><button onclick=\"location.href='/select?ssid=" + urlEncode(ssid) + "'\">" + ssid + "</button></td><td>" + (openNetwork ? "(Open)" : "(Secured)") + "</td><td>" + String(rssi) + " dBm</td></tr>"; | ||||||
|  |     } | ||||||
|  |     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 = "<!DOCTYPE html><html lang='en'><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width, initial-scale=1.0'><title>Connect to " + ssid + "</title><style>body{font-family:Arial,sans-serif;background-color:#f4f4f9;color:#333;display:flex;align-items:center;justify-content:center;height:100vh;margin:0}.container{width:100%;max-width:300px;padding:20px;background:#fff;border-radius:10px;box-shadow:0 4px 8px rgba(0,0,0,0.2);box-sizing:border-box}h2{margin-top:0;text-align:center;color:#0073e6}label{display:block;margin-bottom:5px;font-weight:bold}input[type='password']{width:100%;padding:8px;margin-bottom:15px;border:1px solid #ccc;border-radius:4px;box-sizing:border-box}button{width:100%;padding:10px;background-color:#0073e6;color:white;border:none;border-radius:4px;cursor:pointer;font-size:16px}button:hover{background-color:#005bb5}</style></head><body><div class='container'><h2>Connect to " + ssid + "</h2><form action='/add' method='get'><input type='hidden' name='ssid' value='" + ssid + "'><label for='pass'>Password:</label><input id='pass' type='password' name='pass' placeholder='Enter Password'><button type='submit'>Connect</button></form></div></body></html>"; | ||||||
|  |     server->send(200, "text/html", selectPage); | ||||||
|  | } | ||||||
							
								
								
									
										15
									
								
								src/example_config.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/example_config.h
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||||
							
								
								
									
										189
									
								
								src/main.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										189
									
								
								src/main.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,189 @@ | |||||||
|  | #include <Arduino.h> | ||||||
|  | #include <Adafruit_SSD1306.h> | ||||||
|  | #include <ESP8266HTTPClient.h> | ||||||
|  | #include <ArduinoJson.h> | ||||||
|  | #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<JsonObject>(); | ||||||
|  |   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<JsonArray>(); | ||||||
|  |     int newProblemCount = 0; | ||||||
|  |     memset(severityCounts, 0, sizeof(severityCounts)); // Reset severities | ||||||
|  |  | ||||||
|  |     for (JsonObject problem : result) { | ||||||
|  |       String eventId = problem["eventid"].as<String>(); | ||||||
|  |       int severity = problem["severity"].as<int>(); | ||||||
|  |       String description = problem["name"].as<String>(); | ||||||
|  |  | ||||||
|  |       // 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<String>(); | ||||||
|  |     } | ||||||
|  |   } 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; | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										333
									
								
								src/netman.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										333
									
								
								src/netman.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,333 @@ | |||||||
|  | #include <ESP8266WiFi.h> | ||||||
|  | #include <WiFiClient.h> | ||||||
|  | #include <ESP8266WebServer.h> | ||||||
|  | #include <Adafruit_SSD1306.h> | ||||||
|  | #include <LittleFS.h> | ||||||
|  | #include <DNSServer.h> | ||||||
|  | #include <map> | ||||||
|  |  | ||||||
|  | // Constants for the HTML pages and config file | ||||||
|  | static const String beginHtml = "<!DOCTYPE html><html lang='en'><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width, initial-scale=1.0'><title>SmartCube Configure</title><style>body{font-family:Arial,sans-serif;background-color:#f4f4f9;color:#333;display:flex;align-items:center;justify-content:center;height:100vh;margin:0}.container{width:100%;max-width:300px;padding:20px;background:#fff;border-radius:10px;box-shadow:0 4px 8px rgba(0,0,0,0.2);box-sizing:border-box}h2{margin-top:0;text-align:center;color:#0073e6}label{display:block;margin-bottom:5px;font-weight:bold}input[type='text'],input[type='password']{width:100%;padding:8px;margin-bottom:15px;border:1px solid #ccc;border-radius:4px;box-sizing:border-box}button{width:100%;padding:10px;background-color:#0073e6;color:white;border:none;border-radius:4px;cursor:pointer;font-size:16px}button:hover{background-color:#005bb5}table{width:100%;margin-top:20px}td{padding:5px;text-align:left}</style></head><body><div class='container'><h2>Configure AP</h2><table><tbody><tr><td><label for='ssid'>SSID</label></td><td><input id='ssid' type='text' placeholder='Enter SSID'/></td></tr><tr><td><label for='pass'>Password</label></td><td><input id='pass' type='password' placeholder='Enter Password'/></td></tr><tr><td colspan='2'><button onclick=\"location.href = '/add?ssid=' + encodeURIComponent(document.getElementById('ssid').value) + '&pass=' + encodeURIComponent(document.getElementById('pass').value);\">Add Network</button></td></tr></tbody></table><br/><table><tbody>"; | ||||||
|  | static const String endHtml = "</tbody></table></body></html>"; | ||||||
|  | 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<ESP8266WebServer> server; | ||||||
|  |     std::map<String, String> _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", "<html><body><h2>Connection failed. Please try again.</h2></body></html>"); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 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 += "<h3>Saved Networks</h3>"; | ||||||
|  |     for (const auto& item : _ssids) { | ||||||
|  |         result += "<tr><td><button onclick=\"location.href='/remove?ssid=' + escape('" + item.first + "') + '&pass=' + escape('" + item.second + "') \">×</button></td><td>" + item.first + "</td><td>-</td><td>" + item.second + "</td></tr>"; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Display available WiFi networks | ||||||
|  |     result += "</tbody></table><h3>Available Networks</h3><table><tbody>"; | ||||||
|  |     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 += "<tr><td><button onclick=\"location.href='/select?ssid=" + urlEncode(ssid) + "'\">" + ssid + "</button></td><td>" + (openNetwork ? "(Open)" : "(Secured)") + "</td><td>" + String(rssi) + " dBm</td></tr>"; | ||||||
|  |     } | ||||||
|  |     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 = "<!DOCTYPE html><html lang='en'><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width, initial-scale=1.0'><title>Connect to " + ssid + "</title><style>body{font-family:Arial,sans-serif;background-color:#f4f4f9;color:#333;display:flex;align-items:center;justify-content:center;height:100vh;margin:0}.container{width:100%;max-width:300px;padding:20px;background:#fff;border-radius:10px;box-shadow:0 4px 8px rgba(0,0,0,0.2);box-sizing:border-box}h2{margin-top:0;text-align:center;color:#0073e6}label{display:block;margin-bottom:5px;font-weight:bold}input[type='password']{width:100%;padding:8px;margin-bottom:15px;border:1px solid #ccc;border-radius:4px;box-sizing:border-box}button{width:100%;padding:10px;background-color:#0073e6;color:white;border:none;border-radius:4px;cursor:pointer;font-size:16px}button:hover{background-color:#005bb5}</style></head><body><div class='container'><h2>Connect to " + ssid + "</h2><form action='/add' method='get'><input type='hidden' name='ssid' value='" + ssid + "'><label for='pass'>Password:</label><input id='pass' type='password' name='pass' placeholder='Enter Password'><button type='submit'>Connect</button></form></div></body></html>"; | ||||||
|  |     server->send(200, "text/html", selectPage); | ||||||
|  | } | ||||||
							
								
								
									
										11
									
								
								test/README
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								test/README
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||||
		Reference in New Issue
	
	Block a user