[{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/001/570/original/digital_clock.jpg?1738944951","metadata":{"caption":""}},{"element_type":"code","content":"#include \u003cWire.h\u003e\n#include \u003cAdafruit_GFX.h\u003e\n#include \u003cAdafruit_SH110X.h\u003e\n#include \u003cESP8266WiFi.h\u003e\n#include \u003cWiFiUdp.h\u003e\n#include \u003cNTPClient.h\u003e\n#include \u003cTimeLib.h\u003e\n#include \u003cFonts/FreeSansBold18pt7b.h\u003e\n#include \u003cArduinoJson.h\u003e\n\n#define I2C_ADDRESS   0x3C\n\n// Custom weather icons (16x16 pixels)\nconst uint8_t clearSkyIcon[] PROGMEM = {\n    0b00000000, 0b00000000,\n    0b00000000, 0b00000000,\n    0b00000011, 0b11000000,\n    0b00001111, 0b11110000,\n    0b00011111, 0b11111000,\n    0b00111111, 0b11111100,\n    0b00111111, 0b11111100,\n    0b01111111, 0b11111110,\n    0b01111111, 0b11111110,\n    0b01111111, 0b11111110,\n    0b00111111, 0b11111100,\n    0b00111111, 0b11111100,\n    0b00011111, 0b11111000,\n    0b00001111, 0b11110000,\n    0b00000011, 0b11000000,\n    0b00000000, 0b00000000\n};\n\nconst uint8_t fewCloudsIcon[] PROGMEM = {\n    0b00000000, 0b00000000,\n    0b00000000, 0b00000000,\n    0b00000011, 0b11000000,\n    0b00001111, 0b11110000,\n    0b00011111, 0b11111000,\n    0b00111111, 0b11111100,\n    0b00111111, 0b11111100,\n    0b01111111, 0b11111110,\n    0b01111111, 0b11111110,\n    0b01111111, 0b11111110,\n    0b00111111, 0b11111100,\n    0b00111100, 0b00111100,\n    0b00011000, 0b00011000,\n    0b00000000, 0b00000000,\n    0b00000000, 0b00000000,\n    0b00000000, 0b00000000\n};\n\nconst uint8_t scatteredCloudsIcon[] PROGMEM = {\n    0b00000000, 0b00000000,\n    0b00000000, 0b00000000,\n    0b00000000, 0b00000000,\n    0b00000000, 0b00000000,\n    0b00000000, 0b00000000,\n    0b00000011, 0b11000000,\n    0b00001111, 0b11110000,\n    0b00011111, 0b11111000,\n    0b00111111, 0b11111100,\n    0b00111111, 0b11111100,\n    0b01111111, 0b11111110,\n    0b01111111, 0b11111110,\n    0b01111111, 0b11111110,\n    0b00111111, 0b11111100,\n    0b00000000, 0b00000000,\n    0b00000000, 0b00000000\n};\n\nconst uint8_t rainIcon[] PROGMEM = {\n    0b00000000, 0b00000000,\n    0b00000000, 0b00000000,\n    0b00000011, 0b11000000,\n    0b00001111, 0b11110000,\n    0b00011111, 0b11111000,\n    0b00111111, 0b11111100,\n    0b00111111, 0b11111100,\n    0b01111111, 0b11111110,\n    0b01111111, 0b11111110,\n    0b01111111, 0b11111110,\n    0b00101010, 0b10101000,\n    0b00010101, 0b01010000,\n    0b00101010, 0b10101000,\n    0b00010101, 0b01010000,\n    0b00000000, 0b00000000,\n    0b00000000, 0b00000000\n};\n\nconst uint8_t thunderstormIcon[] PROGMEM = {\n    0b00000000, 0b00000000,\n    0b00000000, 0b00000000,\n    0b00000011, 0b11000000,\n    0b00001111, 0b11110000,\n    0b00011111, 0b11111000,\n    0b00111111, 0b11111100,\n    0b00111111, 0b11111100,\n    0b01111111, 0b11111110,\n    0b01111111, 0b11111110,\n    0b01111111, 0b11111110,\n    0b00111100, 0b00111100,\n    0b00011000, 0b00011000,\n    0b00110000, 0b00001100,\n    0b00011000, 0b00011000,\n    0b00001100, 0b00110000,\n    0b00000000, 0b00000000\n};\n\nconst uint8_t snowIcon[] PROGMEM = {\n    0b00000000, 0b00000000,\n    0b00000000, 0b00000000,\n    0b00000011, 0b11000000,\n    0b00001111, 0b11110000,\n    0b00011111, 0b11111000,\n    0b00111111, 0b11111100,\n    0b00111111, 0b11111100,\n    0b01111111, 0b11111110,\n    0b01111111, 0b11111110,\n    0b01111111, 0b11111110,\n    0b00101010, 0b10101000,\n    0b00010101, 0b01010000,\n    0b00101010, 0b10101000,\n    0b00010101, 0b01010000,\n    0b00000000, 0b00000000,\n    0b00000000, 0b00000000\n};\n\nconst uint8_t mistIcon[] PROGMEM = {\n    0b00000000, 0b00000000,\n    0b00000000, 0b00000000,\n    0b00000000, 0b00000000,\n    0b00000000, 0b00000000,\n    0b00000000, 0b00000000,\n    0b00000000, 0b00000000,\n    0b00000000, 0b00000000,\n    0b01111111, 0b11111110,\n    0b01111111, 0b11111110,\n    0b00000000, 0b00000000,\n    0b00000000, 0b00000000,\n    0b01111111, 0b11111110,\n    0b01111111, 0b11111110,\n    0b00000000, 0b00000000,\n    0b00000000, 0b00000000,\n    0b00000000, 0b00000000\n};\n\nAdafruit_SH1107 display = Adafruit_SH1107(64, 128, \u0026Wire);\n\nconst char* ssid     = \"Your SSID\";\nconst char* password = \"Your SSID password\";\n\nWiFiUDP ntpUDP;\nNTPClient timeClient(ntpUDP, \"pool.ntp.org\", 0, 60000);\n\n// OpenWeatherMap API settings\nconst String apiKey = \"Your API key\"; // Your API key\nconst String weatherUrl = \"http://api.openweathermap.org/data/2.5/weather?lat=57.19316\u0026lon=-2.11010\u0026units=metric\u0026appid=\" + apiKey;\n\nfloat temperature = 0.0; // Variable to store temperature\nbool temperatureFetchSuccess = false; // Track if temperature fetch was successful\n\nunsigned long lastTemperatureFetchTime = 0; // Track the last time the temperature was fetched\nconst unsigned long temperatureFetchInterval = 5 *60 * 1000; // Fetch temperature every 5 minutes (in milliseconds)\nconst unsigned long retryInterval = 5 * 1000; // Retry after 5 seconds if parsing fails\n\nbool colonVisible = true; // Track whether the colon is visible\nunsigned long lastColonToggleTime = 0; // Track the last time the colon was toggled\n\nString weatherIcon = \"\"; // Variable to store the weather icon code\n\nvoid setup() {\n    Serial.begin(115200);\n    WiFi.begin(ssid, password);\n    Serial.print(\"Connecting to Wi-Fi\");\n\n    // Wi-Fi connection with timeout\n    unsigned long startAttemptTime = millis();\n    while (WiFi.status() != WL_CONNECTED \u0026\u0026 millis() - startAttemptTime \u003c 10000) { // 10-second timeout\n        delay(500);\n        Serial.print(\".\");\n    }\n    if (WiFi.status() != WL_CONNECTED) {\n        Serial.println(\"Failed to connect to Wi-Fi!\");\n    } else {\n        Serial.println(\"Connected!\");\n    }\n\n    // Display initialization with error handling\n    if (!display.begin(I2C_ADDRESS, true)) {\n        Serial.println(\"Display initialization failed!\");\n        while (1); // Halt execution\n    }\n    display.setRotation(1); // Rotate display for landscape mode\n    display.clearDisplay();\n\n    timeClient.begin();\n}\n\nvoid loop() {\n    timeClient.update();\n    time_t rawTime = timeClient.getEpochTime();\n    setTime(rawTime);\n\n    // Clear the entire display to avoid overlapping text\n    display.clearDisplay();\n\n    // Display time in 24-hour format at (2, 30)\n    display.setTextColor(SH110X_WHITE);\n    display.setFont(\u0026FreeSansBold18pt7b);\n\n    char timeStr[6];\n    sprintf(timeStr, \"%02d%02d\", hour(), minute()); // 24-hour format without colon\n    display.setCursor(0, 31); // Adjusted position\n    display.print(timeStr[0]); // First digit of hour\n    display.print(timeStr[1]); // Second digit of hour\n\n    // Toggle colon visibility every second\n    if (millis() - lastColonToggleTime \u003e= 1000) {\n        colonVisible = !colonVisible;\n        lastColonToggleTime = millis();\n    }\n\n    // Display colon if visible\n    if (colonVisible) {\n        display.setCursor(38, 30); // Position for colon\n        display.print(\":\");\n    } else {\n        display.fillRect(40, 10, 10, 20, SH110X_BLACK); // Overwrite colon space to prevent shifting\n    }\n\n    display.setCursor(48, 30); // Maintain fixed spacing\n    display.print(timeStr[2]); // First digit of minute\n    display.print(timeStr[3]); // Second digit of minute\n\n    // Display date at (20, 45)\n    display.setFont();\n    display.setTextSize(1);\n    char dateStr[20];\n    sprintf(dateStr, \"%s %02d/%02d/%04d\", dayShortStr(weekday()), day(), month(), year());\n    display.setCursor(20, 45); // Adjusted position\n    display.print(dateStr);\n\n    // Draw Wi-Fi connection status (3 circles)\n    drawWiFiStatus();\n\n    // Fetch and display outdoor temperature at (95, 20)\n    unsigned long currentTime = millis();\n    if (currentTime - lastTemperatureFetchTime \u003e= temperatureFetchInterval || \n        (!temperatureFetchSuccess \u0026\u0026 currentTime - lastTemperatureFetchTime \u003e= retryInterval)) {\n        fetchTemperature();\n        lastTemperatureFetchTime = currentTime; // Update the last fetch time\n    }\n    displayTemperature();\n\n    // Draw weather icon at (95, 10)\n    if (weatherIcon.length() \u003e 0) {\n        drawWeatherIcon(weatherIcon);\n    }\n\n    // Update the display\n    display.display();\n    delay(500); // Update every 500ms\n}\n\nvoid drawWiFiStatus() {\n    if (WiFi.status() == WL_CONNECTED) {\n        // Draw 3 circles for connected status\n        display.drawCircle(126, 1, 6, SH110X_WHITE); // Outer circle\n        display.drawCircle(127, 0, 3, SH110X_WHITE); // Middle circle\n        display.drawCircle(128, 0, 1, SH110X_WHITE); // Inner circle\n    } else {\n        // Draw an \"X\" for disconnected status\n        display.drawLine(115, 0, 125, 10, SH110X_WHITE); // Diagonal line 1\n        display.drawLine(125, 0, 115, 10, SH110X_WHITE); // Diagonal line 2\n    }\n}\n\nvoid fetchTemperature() {\n    if (WiFi.status() == WL_CONNECTED) {\n        WiFiClient client;\n        if (client.connect(\"api.openweathermap.org\", 80)) {\n            // Send HTTP GET request\n            client.print(\"GET \" + weatherUrl + \" HTTP/1.1\\r\\n\");\n            client.print(\"Host: api.openweathermap.org\\r\\n\");\n            client.print(\"Connection: close\\r\\n\\r\\n\");\n\n            // Wait for the response\n            while (client.connected() \u0026\u0026 !client.available()) {\n                delay(10);\n            }\n\n            // Read the response\n            String jsonResponse = \"\";\n            while (client.available()) {\n                jsonResponse += client.readString();\n            }\n            client.stop();\n\n            // Debug: Print the raw JSON response\n            Serial.println(\"Raw JSON Response:\");\n            Serial.println(jsonResponse);\n\n            // Find the start of the JSON object\n            int jsonStart = jsonResponse.indexOf('{');\n            if (jsonStart == -1) {\n                Serial.println(\"JSON start not found!\");\n                temperatureFetchSuccess = false;\n                return;\n            }\n\n            // Extract the JSON object\n            String jsonObject = jsonResponse.substring(jsonStart);\n\n            // Debug: Print the extracted JSON object\n            Serial.println(\"Extracted JSON Object:\");\n            Serial.println(jsonObject);\n\n            // Parse the JSON object\n            DynamicJsonDocument doc(1024);\n            DeserializationError error = deserializeJson(doc, jsonObject);\n            if (error) {\n                Serial.println(\"Failed to parse JSON!\");\n                Serial.println(error.c_str());\n                temperatureFetchSuccess = false;\n                return;\n            }\n\n            // Check if the \"main\" and \"temp\" fields exist\n            if (doc.containsKey(\"main\") \u0026\u0026 doc[\"main\"].containsKey(\"temp\")) {\n                temperature = doc[\"main\"][\"temp\"].as\u003cfloat\u003e(); // Extract temperature\n                temperatureFetchSuccess = true;\n                Serial.println(\"Temperature fetched successfully: \" + String(temperature));\n            } else {\n                Serial.println(\"Temperature data not found in JSON!\");\n                temperatureFetchSuccess = false;\n            }\n\n            // Extract weather icon code\n            if (doc.containsKey(\"weather\") \u0026\u0026 doc[\"weather\"].is\u003cJsonArray\u003e()) {\n                JsonArray weatherArray = doc[\"weather\"].as\u003cJsonArray\u003e();\n                if (weatherArray.size() \u003e 0 \u0026\u0026 weatherArray[0].containsKey(\"icon\")) {\n                    weatherIcon = weatherArray[0][\"icon\"].as\u003cString\u003e();\n                    Serial.println(\"Weather icon code: \" + weatherIcon);\n                } else {\n                    Serial.println(\"Weather icon code not found!\");\n                }\n            }\n        } else {\n            Serial.println(\"Failed to connect to OpenWeatherMap!\");\n            temperatureFetchSuccess = false;\n        }\n    } else {\n        Serial.println(\"Wi-Fi not connected!\");\n        temperatureFetchSuccess = false;\n    }\n}\n\nvoid displayTemperature() {\n    display.setFont();\n    display.setTextSize(1);\n    display.setCursor(92, 25); // Adjusted position\n    if (temperatureFetchSuccess) {\n        display.print(temperature, 1); // Display temperature with 1 decimal place\n        display.print((char)247); // Degrees symbol (ASCII 247)\n        display.print(\"C\"); // Celsius symbol\n    } else {\n        display.print(\"   \"); // Display \"N/A\" if temperature fetch failed\n    }\n}\n\n\nvoid drawWeatherIcon(const String\u0026 iconCode) {\n    const uint8_t* icon = nullptr;\n\n    if (iconCode == \"01d\" || iconCode == \"01n\") {\n        icon = clearSkyIcon; // Clear sky\n    } else if (iconCode == \"02d\" || iconCode == \"02n\") {\n        icon = fewCloudsIcon; // Few clouds\n    } else if (iconCode == \"03d\" || iconCode == \"03n\") {\n        icon = scatteredCloudsIcon; // Scattered clouds\n    } else if (iconCode == \"04d\" || iconCode == \"04n\") {\n        icon = scatteredCloudsIcon; // Broken clouds (same as scattered)\n    } else if (iconCode == \"09d\" || iconCode == \"09n\") {\n        icon = rainIcon; // Shower rain\n    } else if (iconCode == \"10d\" || iconCode == \"10n\") {\n        icon = rainIcon; // Rain (same as shower rain)\n    } else if (iconCode == \"11d\" || iconCode == \"11n\") {\n        icon = thunderstormIcon; // Thunderstorm\n    } else if (iconCode == \"13d\" || iconCode == \"13n\") {\n        icon = snowIcon; // Snow\n    } else if (iconCode == \"50d\" || iconCode == \"50n\") {\n        icon = mistIcon; // Mist\n    } else {\n        // Unknown weather\n        return;\n    }\n\n    // Draw the icon at (97, 2)\n    display.drawBitmap(97, 2, icon, 16, 16, SH110X_WHITE);\n}","metadata":{"language":"auto","linenums":false}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003cp\u003eI had an Adafruit Huzzah and an OLED FeatherWing 128 x 64 in my box of goodies and decided to make a smart digital clock for my study.\u003c/p\u003e\n\u003cp\u003eThe clock is connected to internet via WiFi, synchronises the time and via an api displays the temperature in my area. It is set to update every 5 minutes. Also displays an icon of the current weather.\u0026nbsp;\u003c/p\u003e\n\u003cp\u003eThese are parsed from the api response:\u003c/p\u003e\n\u003cpre\u003e{\"coord\":{\"lon\":yyyyyy,\"lat\":xxxxx},\"weather\":[{\"id\":800,\"main\":\"Clear\",\"description\":\"clear sky\",\"icon\":\"01n\"}],\"base\":\"stations\",\"main\":{\"temp\":1.03,\"feels_like\":-1.33,\"temp_min\":0.57,\"temp_max\":2.64,\"pressure\":1039,\"humidity\":76,\"sea_level\":1039,\"grnd_level\":1034},\"visibility\":10000,\"wind\":{\"speed\":2.06,\"deg\":170},\"clouds\":{\"all\":0},\"dt\":1738790391,\"sys\":{\"type\":1,\"id\":1440,\"country\":\"GB\",\"sunrise\":1738742403,\"sunset\":1738773892},\"timezone\":0,\"id\":3333224,\"name\":\"xxxxxx City\",\"cod\":200}\u003cbr\u003e\u003cbr\u003e\u003cbr\u003e\u003c/pre\u003e\n      \n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"text","content":"\n  \n        \u003cp\u003eNext I created a 3D printed bezel that allowed me to fit the stacked boards in my workbench pannel.\u003c/p\u003e\n      \n","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/001/663/original/WhatsApp_Image_2025-03-05_at_18.29.51_ce1e3c9f.jpg?1741199424","metadata":{"caption":""}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/001/664/original/bezel.JPG?1741199678","metadata":{"caption":""}}]