initial commit

This commit is contained in:
Tomislav Kopić 2024-11-06 21:20:12 +01:00
commit ddf764b658
18 changed files with 887 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
.pio
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch
config.h

10
.vscode/extensions.json vendored Normal file
View 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"
]
}

5
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"files.associations": {
"string": "cpp"
}
}

39
include/README Normal file
View 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
View 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

18
platformio.ini Normal file
View File

@ -0,0 +1,18 @@
; 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
lib_deps =
bblanchon/ArduinoJson@^7.2.0
adafruit/Adafruit SSD1306@^2.5.13
arduino-libraries/NTPClient@^3.2.1

BIN
src/Bitmaps/Clear.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 B

BIN
src/Bitmaps/Drizzle.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 B

BIN
src/Bitmaps/Fog.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 481 B

BIN
src/Bitmaps/Hail.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 482 B

BIN
src/Bitmaps/Rain.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 B

BIN
src/Bitmaps/Snow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 B

71
src/bitmaps.h Normal file
View File

@ -0,0 +1,71 @@
// 'clear', 16x16px
const unsigned char bitmap_clear [] PROGMEM = {
0x01, 0x80, 0x01, 0x80, 0x31, 0x8c, 0x38, 0x1c, 0x1f, 0xf8, 0x0f, 0xf0, 0x0c, 0x30, 0xec, 0x37,
0xec, 0x37, 0x0c, 0x30, 0x0f, 0xf0, 0x1f, 0xf8, 0x38, 0x1c, 0x31, 0x8c, 0x01, 0x80, 0x01, 0x80
};
// 'cloudy', 16x16px
const unsigned char bitmap_cloudy [] PROGMEM = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xf0, 0x03, 0xf8, 0x0f, 0x9e, 0x1f, 0xcf, 0x78, 0xf3,
0xf0, 0x7b, 0xc0, 0x1f, 0xc0, 0x1f, 0xff, 0xf8, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
// 'fog', 16x16px
const unsigned char bitmap_fog [] PROGMEM = {
0x00, 0x00, 0x78, 0x1e, 0xff, 0xff, 0xef, 0xf7, 0x00, 0x00, 0xfe, 0x7f, 0xff, 0xff, 0x03, 0xc0,
0x7c, 0x3e, 0xff, 0xff, 0xcf, 0xf3, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x01, 0x80, 0x00, 0x00
};
// 'rain', 16x16px
const unsigned char bitmap_rain [] PROGMEM = {
0x0f, 0x80, 0x1f, 0xc0, 0x38, 0xf8, 0x70, 0x7c, 0xf0, 0x0c, 0xe0, 0x0f, 0xc0, 0x07, 0xc0, 0x03,
0xc0, 0x03, 0xff, 0xff, 0x7f, 0xfe, 0x66, 0x33, 0xe6, 0x73, 0xff, 0xff, 0xff, 0xfe, 0x73, 0x38
};
// 'snow', 16x16px
const unsigned char bitmap_snow [] PROGMEM = {
0x01, 0x80, 0x07, 0xe0, 0x1f, 0xf8, 0x7b, 0xde, 0x79, 0x9e, 0x7d, 0xbe, 0x7f, 0xfe, 0x67, 0xe6,
0x67, 0xe6, 0x7f, 0xfe, 0x7d, 0xbe, 0x79, 0x9e, 0x7b, 0xde, 0x1f, 0xf8, 0x07, 0xe0, 0x01, 0x80
};
// 'thunderstorm', 16x16px
const unsigned char bitmap_thunderstorm [] PROGMEM = {
0x07, 0xf0, 0x06, 0x60, 0x0e, 0x60, 0x0c, 0xc0, 0x0c, 0xf8, 0x18, 0xfc, 0x18, 0x18, 0x38, 0x38,
0x3f, 0xf0, 0x1f, 0xe0, 0x03, 0xe0, 0x03, 0xc0, 0x03, 0xc0, 0x03, 0x80, 0x07, 0x00, 0x07, 0x00
};
// 'drizzle', 16x16px
const unsigned char bitmap_drizzle [] PROGMEM = {
0x00, 0x00, 0x06, 0x00, 0x36, 0xc0, 0x3f, 0x80, 0x1f, 0xf0, 0x7b, 0xb8, 0x1e, 0x18, 0x18, 0x0e,
0x18, 0x06, 0x19, 0x32, 0x0f, 0xfe, 0x07, 0xfc, 0x01, 0xf0, 0x03, 0xf0, 0x00, 0xc0, 0x00, 0x00
};
// Array of all bitmaps for convenience
const unsigned char* bitmap_icons[7] = {
bitmap_clear,
bitmap_cloudy,
bitmap_fog,
bitmap_rain,
bitmap_snow,
bitmap_drizzle,
bitmap_thunderstorm,
};
const unsigned char temperature_icon [] PROGMEM = {
0x1e, 0x00, 0x1e, 0x00, 0x1e, 0x00, 0x1e, 0x00, 0x1e, 0x00, 0x1e, 0x00, 0x1e, 0x00, 0x3f, 0x00,
0x3f, 0x00, 0x1e, 0x00
};
const unsigned char humidity_icon [] PROGMEM = {
0x0c, 0x00, 0x1e, 0x00, 0x1e, 0x00, 0x3f, 0x00, 0x7f, 0x80, 0x7f, 0x80, 0x7f, 0x80, 0x7f, 0x80,
0x3f, 0x00, 0x3f, 0x00
};
const unsigned char wind_icon [] PROGMEM = {
0x00, 0x00, 0x07, 0x00, 0x07, 0x80, 0xff, 0xc0, 0xff, 0xc0, 0xff, 0xc0, 0xff, 0xc0, 0x07, 0x80,
0x07, 0x00, 0x00, 0x00
};
const unsigned char pressure_icon [] PROGMEM = {
0x1e, 0x00, 0x3f, 0x00, 0x73, 0x80, 0xe7, 0xc0, 0xce, 0xc0, 0xdc, 0xc0, 0xe9, 0xc0, 0x73, 0x80,
0x3f, 0x00, 0x1e, 0x00
};

15
src/example_config.h Normal file
View File

@ -0,0 +1,15 @@
// OpenWeatherMap settings
#define LOCATION ""
#define API_KEY ""
// 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

333
src/main.cpp Normal file
View File

@ -0,0 +1,333 @@
#include "config.h"
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <ArduinoJson.h>
#include <Wire.h>
#include <NTPClient.h>
#include <WiFiUdp.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include "bitmaps.h"
#include "netman.h"
// Init display and wifi
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
netman netman(display);
WiFiClient client;
// Define NTP Client to get time
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "pool.ntp.org", 3600);
// Define these in config.h
const String Location = LOCATION;
const String API_Key = API_KEY;
void capitalizeFirstLetter(char* str) {
if (str != nullptr && *str != '\0') {
str[0] = toupper(str[0]);
}
}
void beep(int buzz) {
tone(PIN_BUZZER, buzz, 100);
delay(100);
noTone(PIN_BUZZER);
}
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);
// 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();
}
// Timers for dynamic updates
unsigned long lastWeatherUpdate = 0;
unsigned long lastDisplayUpdate = 0;
const unsigned long weatherUpdateInterval = 1800000; // 30 minutes in milliseconds
const unsigned long displayOffInterval = 300000; // 5 minutes in milliseconds for power-saving mode
bool is_display_off = false;
void commonButtonHandler() {
unsigned long currentMillis = millis();
static unsigned long leftPressStart = 0;
static unsigned long middlePressStart = 0;
static unsigned long rightPressStart = 0;
static bool leftBeeped = false;
static bool middleBeeped = false;
static bool rightBeeped = false;
bool leftPressed = (digitalRead(PIN_BTN_L) == HIGH);
bool middlePressed = (digitalRead(PIN_BTN_M) == HIGH);
bool rightPressed = (digitalRead(PIN_BTN_R) == HIGH);
// Short press detection
if (leftPressed) {
if ((currentMillis - leftPressStart > 50)) { // Debounce delay
if (!leftBeeped) {
beep(1000); // Play beep sound
leftBeeped = true; // Set beeped state
// Handle left short press action here
}
}
} else {
leftPressStart = currentMillis; // Reset the timer if button is not pressed
leftBeeped = false; // Reset beeped state
}
if (middlePressed) {
if ((currentMillis - middlePressStart > 50)) { // Debounce delay
if (!middleBeeped) {
beep(1000); // Play beep sound
middleBeeped = true; // Set beeped state
// Handle middle short press action here
}
}
} else {
middlePressStart = currentMillis; // Reset the timer if button is not pressed
middleBeeped = false; // Reset beeped state
}
if (rightPressed) {
if ((currentMillis - rightPressStart > 50)) { // Debounce delay
if (!rightBeeped) {
beep(1000); // Play beep sound
rightBeeped = true; // Set beeped state
if (is_display_off) {
display.ssd1306_command(SSD1306_DISPLAYON); // Turn on display
is_display_off = false;
beep(1300);
}
}
}
} else {
rightPressStart = currentMillis; // Reset the timer if button is not pressed
rightBeeped = false; // Reset beeped state
}
// Long press detection
if (leftPressed && (currentMillis - leftPressStart > 2000)) {
if (!leftBeeped) {
beep(1000); // Play beep sound
leftBeeped = true; // Set beeped state
// Handle left long press action here
}
}
if (middlePressed && (currentMillis - middlePressStart > 2000)) {
if (!middleBeeped) {
beep(1000); // Play beep sound
middleBeeped = true; // Set beeped state
// Handle middle long press action here
}
}
if (rightPressed && (currentMillis - rightPressStart > 2000)) {
if (!is_display_off) {
beep(1300);
display.ssd1306_command(SSD1306_DISPLAYOFF); // Turn off display
is_display_off = true;
beep(1000);
}
}
// Combination of Left and Middle long press
if (leftPressed && middlePressed &&
(currentMillis - leftPressStart > 2000) && (currentMillis - middlePressStart > 2000)) {
ESP.restart();
}
}
void powerSaveCheck() {
int currentHour = timeClient.getHours();
if ((currentHour >= 23 || currentHour < 6) && !is_display_off) {
display.ssd1306_command(SSD1306_DISPLAYOFF); // Turn off display for power-saving
is_display_off = true;
lastDisplayUpdate = millis(); // Track when display was turned off
} else if (is_display_off && (millis() - lastDisplayUpdate > displayOffInterval)) {
display.ssd1306_command(SSD1306_DISPLAYON); // Turn display back on
is_display_off = false;
}
}
int icon = 0;
bool fetchWeatherData() {
// Get current time
time_t epochTime = timeClient.getEpochTime();
struct tm *ptm = gmtime((time_t *)&epochTime);
int currentDay = ptm->tm_mday;
int currentMonth = ptm->tm_mon + 1;
int currentYear = ptm->tm_year + 1900;
HTTPClient http;
int attempt;
bool dataFetched = false;
// Request current weather
for (attempt = 0; attempt < 3; attempt++) {
// Request current weather
http.begin(client, "http://api.openweathermap.org/data/2.5/weather?q=" + Location + "&APPID=" + API_Key);
int httpCode = http.GET();
if (httpCode > 0) {
String payload = http.getString();
DynamicJsonDocument doc(1024);
DeserializationError error = deserializeJson(doc, payload);
if (!error) {
const char* state = doc["weather"][0]["description"];
capitalizeFirstLetter(const_cast<char*>(state));
float temp = doc["main"]["temp"].as<float>() - 273.15;
int humidity = doc["main"]["humidity"];
float pressure = doc["main"]["pressure"].as<float>() / 1000;
float wind_speed = doc["wind"]["speed"].as<float>();
int statusSize = strlen(state) > 12 ? 1 : 2;
// Display data
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0);
display.printf("%d-%d-%d\r\n", currentYear, currentMonth, currentDay);
display.setCursor(0, 9);
display.printf("%s", Location.c_str());
display.setTextSize(statusSize);
display.setCursor(0, 21);
display.printf("%s\r\n", state);
display.setCursor(0, 39);
display.setTextSize(1);
display.printf(" %5.2f C %d%%\r\n", temp, humidity);
display.drawRect(43, 39, 3, 3, WHITE); // Degree symbol
display.setCursor(0, 52);
display.printf(" %.3fbar %.1fm/s \r\n", pressure, wind_speed);
display.drawLine(0, 18, 127, 18, 1);
display.drawLine(65, 18, 65, 0, 1);
display.drawBitmap(0, 38, temperature_icon, 10, 10, WHITE);
display.drawBitmap(74, 38, humidity_icon, 10, 10, WHITE);
display.drawBitmap(0, 51, pressure_icon, 10, 10, WHITE);
display.drawBitmap(74, 51, wind_icon, 10, 10, WHITE);
display.display();
http.end();
dataFetched = true;
break; // Exit the loop after successful fetch
}
}
// Display attempt number
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0);
display.printf("Attempt %d/3", attempt + 1);
display.display();
delay(1000); // Retry after 1 second if the request fails
}
if (!dataFetched) {
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0);
display.printf("Failed to fetch weather");
display.display();
http.end();
return false; // Return false if the fetch fails after 3 attempts
}
// Request forecast
for (attempt = 0; attempt < 3; attempt++) {
// Request forecast
http.begin(client, "http://api.openweathermap.org/data/2.5/forecast?q=" + Location + "&APPID=" + API_Key + "&cnt=3");
int httpCode = http.GET();
if (httpCode > 0) {
String payload = http.getString();
DynamicJsonDocument forecastDoc(1024);
DeserializationError error = deserializeJson(forecastDoc, payload);
if (!error) {
int pos = 69;
for (int day = 0; day <= 2; day++) {
int state = forecastDoc["list"][day]["weather"][0]["id"];
// Determine icon based on state ID
if (state >= 200 && state <= 232) {
icon = 6; // Thunderstorm
} else if (state >= 300 && state <= 321) {
icon = 5; // Drizzle
} else if (state >= 500 && state <= 531) {
icon = 3; // Rain
} else if (state >= 600 && state <= 622) {
icon = 4; // Snow
} else if (state >= 701 && state <= 781) {
icon = 2; // Mist
} else if (state == 800) {
icon = 0; // Clear
} else if (state >= 801 && state <= 804) {
icon = 1; // Clouds
} else {
icon = 0; // Default icon
}
display.drawBitmap(pos, 0, bitmap_icons[icon], 16, 16, WHITE);
pos += 20;
display.display();
}
http.end();
break; // Exit the loop after successful fetch
}
}
// Display attempt number for forecast
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0);
display.printf("Forecast attempt %d/3", attempt + 1);
display.display();
delay(1000); // Retry after 1 second if the request fails
}
if (!dataFetched) {
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0);
display.printf("Failed to fetch forecast");
display.display();
return false; // Return false if the forecast fetch fails after 3 attempts
}
return true; // Return true if both data fetches were successful
}
void setup(void) {
initSystems();
netman.start();
timeClient.begin();
delay(500);
timeClient.setTimeOffset(3600);
}
void loop() {
timeClient.update();
if (!is_display_off && (millis() - lastWeatherUpdate > weatherUpdateInterval)) {
fetchWeatherData();
lastWeatherUpdate = millis();
}
powerSaveCheck();
commonButtonHandler();
}

333
src/netman.h Normal file
View 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 + "') \">&times;</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
View 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