From ddf764b65860f3349ba8262c5a10e46b88dbbd73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomislav=20Kopi=C4=87?= Date: Wed, 6 Nov 2024 21:20:12 +0100 Subject: [PATCH] initial commit --- .gitignore | 6 + .vscode/extensions.json | 10 + .vscode/settings.json | 5 + include/README | 39 ++++ lib/README | 46 +++++ platformio.ini | 18 ++ src/Bitmaps/Clear.png | Bin 0 -> 393 bytes src/Bitmaps/Drizzle.png | Bin 0 -> 382 bytes src/Bitmaps/Fog.png | Bin 0 -> 481 bytes src/Bitmaps/Hail.png | Bin 0 -> 482 bytes src/Bitmaps/Rain.png | Bin 0 -> 436 bytes src/Bitmaps/Snow.png | Bin 0 -> 406 bytes src/Bitmaps/Thunderstorms.png | Bin 0 -> 370 bytes src/bitmaps.h | 71 ++++++++ src/example_config.h | 15 ++ src/main.cpp | 333 ++++++++++++++++++++++++++++++++++ src/netman.h | 333 ++++++++++++++++++++++++++++++++++ test/README | 11 ++ 18 files changed, 887 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 include/README create mode 100644 lib/README create mode 100644 platformio.ini create mode 100644 src/Bitmaps/Clear.png create mode 100644 src/Bitmaps/Drizzle.png create mode 100644 src/Bitmaps/Fog.png create mode 100644 src/Bitmaps/Hail.png create mode 100644 src/Bitmaps/Rain.png create mode 100644 src/Bitmaps/Snow.png create mode 100644 src/Bitmaps/Thunderstorms.png create mode 100644 src/bitmaps.h create mode 100644 src/example_config.h create mode 100644 src/main.cpp create mode 100644 src/netman.h create mode 100644 test/README diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..64727e5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch +config.h \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..080e70d --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "platformio.platformio-ide" + ], + "unwantedRecommendations": [ + "ms-vscode.cpptools-extension-pack" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..d8cb326 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "files.associations": { + "string": "cpp" + } +} \ No newline at end of file diff --git a/include/README b/include/README new file mode 100644 index 0000000..194dcd4 --- /dev/null +++ b/include/README @@ -0,0 +1,39 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the usual convention is to give header files names that end with `.h'. +It is most portable to use only letters, digits, dashes, and underscores in +header file names, and at most one dot. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/lib/README b/lib/README new file mode 100644 index 0000000..2593a33 --- /dev/null +++ b/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into executable file. + +The source code of each library should be placed in an own separate directory +("lib/your_library_name/[here are source files]"). + +For example, see a structure of the following two libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +and a contents of `src/main.c`: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +PlatformIO Library Dependency Finder will find automatically dependent +libraries scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..2c1a82d --- /dev/null +++ b/platformio.ini @@ -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 diff --git a/src/Bitmaps/Clear.png b/src/Bitmaps/Clear.png new file mode 100644 index 0000000000000000000000000000000000000000..4c25681e6ad079008403674193ef35b7e076d618 GIT binary patch literal 393 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`EX7WqAsj$Z!;#Vf2?- z-5|_3H?j$&NV3E=q9iy!t)x7$D3zhSyj(9cFS|H7u^?41zbJk7I~yqm21Yhd7sn8b z-rmcGeuo`IST1bun8tQ7x>3z!XY*{6B^*M$>K@%wx|cj$b$Y|5zg${ujtX7AYnQBgO3dtN(f)js zXFm??(9GyOKGWFVNF~%O i7miE*>eXkiwYT+DUvjx+?ge15F?hQAxvX9ICjhC(snxQy2+$6(7N#MX`|zDpIU*J#;4Fy3TQz(g;(BRbA z5Y6rV9}P{>AJB9NK_U#HDHQ}88p`3o(TqNDFbIs>;k{2uoLWxL_VV2C^E}seMP}+} z4MIc&&R_z&nc3+z91#;3K?k2QGXoFt0ORH1vB%)Op-nJFSp;gTlfHeP4u!5;?>;)bSH z2b-DsHSiesi@jY=ju+C=jI;QGt#UPrCsEJb&CGXoQ{cKL(=P-~#y45a5pe+{|0UxT z?&0zg2Q4gNwdSt*ow4g+15djSe%2G)nq&*beXiX$9)31X= X8sL6ZYAMnJ00000NkvXXu0mjftp3Mz literal 0 HcmV?d00001 diff --git a/src/Bitmaps/Hail.png b/src/Bitmaps/Hail.png new file mode 100644 index 0000000000000000000000000000000000000000..dc48ca5c0fe6e8a59a235c32e60eee52261d5a30 GIT binary patch literal 482 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`EX7WqAsj$Z!;#Vf2?- zNDyXRHFsqjkRe&(8c`CQpH@mmtT}V`<;yx0|R5cr;B5V zMQ`mTdylX{3APXSzyH)+qS(~cF)5+hxviz;!2-1#VJe;8Yt2}48=Wm~2(5S^z~LvP ztJ&1e;+L0X)-n0Ql5g&UQc4phENUxW7nAq>myZ7PJ?|^eeYUd+{pT)OThVj>$0Xz8 z#-KI4JGuq0B(Y0Zbbho5?q2?*Z)wYdrpTD1W^oTpz8+Lux6oeb>{0()A516SZklWn za#G{YajA6)Jkc}U(xVRQ*Bq;^@&0MhZ_M*By{&@3e1oP}^$f`!ec@e!hqPXuYKm0e zSSbJfgZjIBF%BQXKjdxdkza8nnbzwU7h1mK|THwqI;wc=gB#;^Ko(XwaV-{ zovn5B literal 0 HcmV?d00001 diff --git a/src/Bitmaps/Rain.png b/src/Bitmaps/Rain.png new file mode 100644 index 0000000000000000000000000000000000000000..0429782f475214a6dc0d418696801e1a33553f45 GIT binary patch literal 436 zcmV;l0ZaagP)xJk(<#5{s0}sz}#ubm{J< z8%Uze2BV!)%*vup*+eN4%iX1n#4T9FaLXCKzo)*Q=aC=p?fd?|KmXs9VhksEz%P6) zqj?dN_=7>zU;w`{lP0(lH^>+9Ex5-W&M{FgxB}mkmdl|Vjp)NQhU3qA9N-Y+DLVaz zjhI-*3Nk5z;UgZghT06S(2vWM37`Q>_zxFoenW7ahT|5}cn_Ql40p%1~Zju9umYU7Va)kgAtols@~NjTBH(a)3{W z>;M1%flQi$`fVTX0WDA{3GxeOU}Te!QBv11G%~jHj!sUi>*(&8Fmd|49s3WTJazl? z7sH01Yk_JjJzX3_B*Jt1J^308c$nF}Zk`e=soGnxG_kNleZRGNNP{&jW3K36NKbLh*2~7Yc C_;DKm literal 0 HcmV?d00001 diff --git a/src/Bitmaps/Thunderstorms.png b/src/Bitmaps/Thunderstorms.png new file mode 100644 index 0000000000000000000000000000000000000000..188741ea35a2692c17e7a1aed2fad1474949c408 GIT binary patch literal 370 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`EX7WqAsj$Z!;#Vf2?- zG7x6;t5)LxG9*h}BT9nv(@M${i&7cN%ggmL^RkPR6AM!H@{7`Ezq647Dthnf;uvDl zJNMFFJ(oZc)(81&!5Sw579Mt4sK_P4Wz}zB)Ot*~ql5jzIc*@%)utCF?f@$PGr_nKv|g5M;m6|6d8)86j0zW4Tl>OV<#Vi%6Y9c-$V zIo2j>lWb%(|CsQxWJd2F$CBH4rxYkwo^uK@oR%alJ;}cC-q|^>lIv%FVdQ I&MBb@0B=x=kpKVy literal 0 HcmV?d00001 diff --git a/src/bitmaps.h b/src/bitmaps.h new file mode 100644 index 0000000..835215b --- /dev/null +++ b/src/bitmaps.h @@ -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 +}; diff --git a/src/example_config.h b/src/example_config.h new file mode 100644 index 0000000..60fa796 --- /dev/null +++ b/src/example_config.h @@ -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 \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..ee9cf09 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,333 @@ +#include "config.h" +#include +#include +#include +#include +#include +#include +#include +#include +#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(state)); + + float temp = doc["main"]["temp"].as() - 273.15; + int humidity = doc["main"]["humidity"]; + float pressure = doc["main"]["pressure"].as() / 1000; + float wind_speed = doc["wind"]["speed"].as(); + + 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(); +} diff --git a/src/netman.h b/src/netman.h new file mode 100644 index 0000000..d8c131a --- /dev/null +++ b/src/netman.h @@ -0,0 +1,333 @@ +#include +#include +#include +#include +#include +#include +#include + +// Constants for the HTML pages and config file +static const String beginHtml = "SmartCube Configure

Configure AP


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

Connection failed. Please try again.

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

Saved Networks

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

Available Networks

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

Connect to " + ssid + "

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