From 59426310cf8df3e4599730886a4c3c50beab6533 Mon Sep 17 00:00:00 2001 From: = Date: Fri, 5 Dec 2025 15:21:36 -0600 Subject: [PATCH 1/5] Uplift UI Add date --- examples/weather/src/main.cpp | 292 ++++++++++++++++++++++++++++++---- 1 file changed, 261 insertions(+), 31 deletions(-) diff --git a/examples/weather/src/main.cpp b/examples/weather/src/main.cpp index 91a86b7..f91155f 100644 --- a/examples/weather/src/main.cpp +++ b/examples/weather/src/main.cpp @@ -12,9 +12,13 @@ Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1); #define GAME_WIDTH 64 #define GAME_HEIGHT 128 -const char* ssid = "Wokwi-GUEST"; -const char* pass = ""; -// Hardcoded for Omaha, NE +// Update to your WiFi creds! +//const char* ssid = "Wokwi-GUEST"; +//const char* pass = ""; +const char* ssid = "kame house"; +const char* pass = "spacedicks"; +// Update to your location! +// Default for Omaha, NE const float latitude = 41.2565; const float longitude = -95.9345; @@ -24,6 +28,9 @@ const float longitude = -95.9345; int gmtOffset_sec = -6 * 3600; int daylightOffset_sec = 3600; +// Time format: true for 12-hour, false for 24-hour +bool use12HourFormat = true; + void detectTimezone() { if (WiFi.status() != WL_CONNECTED) return; @@ -51,6 +58,82 @@ String weatherDescription = ""; float temperature = 0.0; String currentTime = ""; +int drawCenteredText(String text, int y, int textSize, int maxWidth) { + display.setTextSize(textSize); + display.setTextColor(SSD1306_WHITE); + + int16_t x1, y1; + uint16_t w, h; + + display.getTextBounds(text, 0, 0, &x1, &y1, &w, &h); + + // Calculate width using character count for default font (6 pixels per char) + int charWidth = text.length() * 6 * textSize; + // Use the larger of the two widths for the wrapping check + int checkW = (charWidth > w) ? charWidth : w; + + if (checkW <= maxWidth) { + int x = (maxWidth - w) / 2; + display.setCursor(x, y); + display.println(text); + return h; + } + + // Split text into words + String words[10]; + int wordCount = 0; + int startIndex = 0; + int spaceIndex; + + while ((spaceIndex = text.indexOf(' ', startIndex)) != -1 && wordCount < 10) { + words[wordCount++] = text.substring(startIndex, spaceIndex); + startIndex = spaceIndex + 1; + } + if (startIndex < text.length() && wordCount < 10) { + words[wordCount++] = text.substring(startIndex); + } + + // Build lines without breaking words + String lines[5]; // Max 5 lines + int lineCount = 0; + String currentLine = ""; + + for (int i = 0; i < wordCount; i++) { + String testLine = currentLine + (currentLine.length() > 0 ? " " : "") + words[i]; + + // Check width of test line using conservative estimate + int testW = testLine.length() * 6 * textSize; + + if (testW <= maxWidth) { + currentLine = testLine; + } else { + if (currentLine.length() > 0) { + lines[lineCount++] = currentLine; + currentLine = words[i]; + } else { + lines[lineCount++] = words[i]; + currentLine = ""; + } + } + } + + if (currentLine.length() > 0 && lineCount < 5) { + lines[lineCount++] = currentLine; + } + + // Draw each line centered + int currentY = y; + for (int i = 0; i < lineCount; i++) { + display.getTextBounds(lines[i], 0, 0, &x1, &y1, &w, &h); + int x = (maxWidth - w) / 2; + display.setCursor(x, currentY); + display.println(lines[i]); + currentY += h + 2; + } + + return currentY - y; +} + String getWeatherDescription(int code) { switch (code) { case 0: return "Clear sky"; @@ -85,10 +168,92 @@ String getWeatherDescription(int code) { } } +void showBootScreen() { + display.clearDisplay(); + + // Show device32 boot logo with border + display.setTextSize(1); + display.setTextColor(SSD1306_WHITE); + + String bootText = "device32"; + int16_t x1, y1; + uint16_t w, h; + display.getTextBounds(bootText, 0, 0, &x1, &y1, &w, &h); + + int textX = (GAME_WIDTH - w) / 2; + int textY = (GAME_HEIGHT - h) / 2; + + // Draw border around text + int padding = 6; + int rectX = textX - padding; + int rectY = textY - padding; + int rectW = w + (padding * 2); + int rectH = h + (padding * 2); + display.drawRoundRect(rectX, rectY, rectW, rectH, 3, SSD1306_WHITE); + + display.setCursor(textX, textY); + display.println(bootText); + + display.display(); + delay(800); +} + void connectToWiFi() { WiFi.begin(ssid, pass); + + int dotCount = 0; + int direction = 1; + unsigned long lastDotUpdate = 0; + while (WiFi.status() != WL_CONNECTED) { - delay(1000); + unsigned long currentMillis = millis(); + + // Update dots every 500ms + if (currentMillis - lastDotUpdate >= 500) { + display.clearDisplay(); + + String connectingText = "WiFi"; + for (int i = 0; i < dotCount; i++) { + connectingText += "."; + } + + display.setTextSize(1); + display.setTextColor(SSD1306_WHITE); + + int16_t x1, y1; + uint16_t w, h; + display.getTextBounds(connectingText, 0, 0, &x1, &y1, &w, &h); + + int textX = (GAME_WIDTH - w) / 2; + int textY = (GAME_HEIGHT - h) / 2; + + String maxText = "WiFi..."; + display.getTextBounds(maxText, 0, 0, &x1, &y1, &w, &h); + int maxTextX = (GAME_WIDTH - w) / 2; + + int padding = 4; + int rectX = maxTextX - padding; + int rectY = textY - padding; + int rectW = w + (padding * 2); + int rectH = h + (padding * 2); + display.drawRoundRect(rectX, rectY, rectW, rectH, 2, SSD1306_WHITE); + + display.setCursor(textX, textY); + display.println(connectingText); + + display.display(); + + dotCount += direction; + if (dotCount >= 3) { + direction = -1; // Start decreasing + } else if (dotCount <= 0) { + direction = 1; // Start increasing + } + + lastDotUpdate = currentMillis; + } + + delay(100); } } @@ -126,8 +291,21 @@ void updateTime() { return; } char buffer[20]; - strftime(buffer, sizeof(buffer), "%H:%M", &timeinfo); - currentTime = String(buffer); + if (use12HourFormat) { + strftime(buffer, sizeof(buffer), "%I:%M", &timeinfo); + currentTime = String(buffer); + // Remove leading zero from hour + if (currentTime[0] == '0') { + currentTime = currentTime.substring(1); + } + } else { + strftime(buffer, sizeof(buffer), "%H:%M", &timeinfo); + currentTime = String(buffer); + // Remove leading zero from hour + if (currentTime[0] == '0') { + currentTime = currentTime.substring(1); + } + } } void setup() { @@ -140,6 +318,9 @@ void setup() { display.clearDisplay(); display.display(); + // Show boot screen + showBootScreen(); + connectToWiFi(); detectTimezone(); @@ -168,49 +349,98 @@ void loop() { // Draw display display.clearDisplay(); - display.drawRoundRect(0, 0, GAME_WIDTH, GAME_HEIGHT, 4, SSD1306_WHITE); - // Weather on top + // Time on top display.setTextSize(1); display.setTextColor(SSD1306_WHITE); int16_t x1, y1; uint16_t w, h; - String weatherLabel = "Weather:"; + String timeLabel = "Time"; + display.getTextBounds(timeLabel, 0, 0, &x1, &y1, &w, &h); + int timeLabelX = (GAME_WIDTH - w) / 2; + int timeLabelY = 5; + + // Draw border + int timeBoxPadding = 3; + int timeBoxX = 0; + int timeBoxY = timeLabelY - timeBoxPadding; + int timeBoxW = GAME_WIDTH; + int timeBoxH = h + (timeBoxPadding * 2); + display.drawRoundRect(timeBoxX, timeBoxY, timeBoxW, timeBoxH, 2, SSD1306_WHITE); + + display.setCursor(timeLabelX, timeLabelY); + display.println(timeLabel); + + display.setTextSize(1); // Changed from size 2 to size 1 + display.getTextBounds(currentTime, 0, 0, &x1, &y1, &w, &h); + int timeX = (GAME_WIDTH - w) / 2; + display.setCursor(timeX, 20); + display.println(currentTime); + + // Date closer to time + display.setTextSize(1); + String dateLabel = "Date"; + display.getTextBounds(dateLabel, 0, 0, &x1, &y1, &w, &h); + int dateLabelX = (GAME_WIDTH - w) / 2; + int dateLabelY = 40; + + // Draw border + int dateBoxPadding = 3; + int dateBoxX = 0; + int dateBoxY = dateLabelY - dateBoxPadding; + int dateBoxW = GAME_WIDTH; + int dateBoxH = h + (dateBoxPadding * 2); + display.drawRoundRect(dateBoxX, dateBoxY, dateBoxW, dateBoxH, 2, SSD1306_WHITE); + + display.setCursor(dateLabelX, dateLabelY); + display.println(dateLabel); + + // Get current date + struct tm timeinfo; + if (getLocalTime(&timeinfo)) { + char dateBuffer[20]; + strftime(dateBuffer, sizeof(dateBuffer), "%m/%d/%Y", &timeinfo); + String currentDate = String(dateBuffer); + + display.getTextBounds(currentDate, 0, 0, &x1, &y1, &w, &h); + int dateX = (GAME_WIDTH - w) / 2; + display.setCursor(dateX, 55); + display.println(currentDate); + } + + // Weather closer to date + display.setTextSize(1); + String weatherLabel = "Weather"; display.getTextBounds(weatherLabel, 0, 0, &x1, &y1, &w, &h); - int x = (GAME_WIDTH - w) / 2; - display.setCursor(x, 10); + int weatherLabelX = (GAME_WIDTH - w) / 2; + int weatherLabelY = 80; + + // Draw rounded box around weather label + int boxPadding = 3; + int boxX = 0; + int boxY = weatherLabelY - boxPadding; + int boxW = GAME_WIDTH; + int boxH = h + (boxPadding * 2); + display.drawRoundRect(boxX, boxY, boxW, boxH, 2, SSD1306_WHITE); + + display.setCursor(weatherLabelX, weatherLabelY); display.println(weatherLabel); String desc = weatherDescription.length() == 0 ? "Loading weather..." : weatherDescription; - display.getTextBounds(desc, 0, 0, &x1, &y1, &w, &h); - x = (GAME_WIDTH - w) / 2; - display.setCursor(x, 25); - display.println(desc); + int weatherHeight = drawCenteredText(desc, 95, 1, GAME_WIDTH); String tempStr = (temperature == 0.0 && weatherDescription.length() == 0) ? "" : String((int)temperature) + " F"; if (tempStr != "") { - display.setTextSize(2); + display.setTextSize(1); // Changed from size 2 to size 1 display.getTextBounds(tempStr, 0, 0, &x1, &y1, &w, &h); - x = (GAME_WIDTH - w) / 2; - display.setCursor(x, 45); + int tempX = (GAME_WIDTH - w) / 2; + int tempY = 93 + weatherHeight + 8; + display.setCursor(tempX, tempY); display.println(tempStr); display.setTextSize(1); } - // Time on bottom - String timeLabel = "Time:"; - display.getTextBounds(timeLabel, 0, 0, &x1, &y1, &w, &h); - x = (GAME_WIDTH - w) / 2; - display.setCursor(x, 85); - display.println(timeLabel); - - display.setTextSize(2); - display.getTextBounds(currentTime, 0, 0, &x1, &y1, &w, &h); - x = (GAME_WIDTH - w) / 2; - display.setCursor(x, 100); - display.println(currentTime); - display.display(); delay(1000); From 244d333fa4bbc19a70c6801e113378fdfa81c1df Mon Sep 17 00:00:00 2001 From: Matthew Faltys Date: Sat, 6 Dec 2025 13:01:35 -0600 Subject: [PATCH 2/5] Fix wifi loading message formatting --- examples/weather/src/main.cpp | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/examples/weather/src/main.cpp b/examples/weather/src/main.cpp index f91155f..79ea776 100644 --- a/examples/weather/src/main.cpp +++ b/examples/weather/src/main.cpp @@ -12,11 +12,9 @@ Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1); #define GAME_WIDTH 64 #define GAME_HEIGHT 128 -// Update to your WiFi creds! -//const char* ssid = "Wokwi-GUEST"; -//const char* pass = ""; -const char* ssid = "kame house"; -const char* pass = "spacedicks"; +// Update to your WiFi creds!WiFi +const char* ssid = "Wokwi-GUEST"; +const char* pass = ""; // Update to your location! // Default for Omaha, NE const float latitude = 41.2565; @@ -201,8 +199,7 @@ void showBootScreen() { void connectToWiFi() { WiFi.begin(ssid, pass); - int dotCount = 0; - int direction = 1; + int step = 0; unsigned long lastDotUpdate = 0; while (WiFi.status() != WL_CONNECTED) { @@ -212,6 +209,7 @@ void connectToWiFi() { if (currentMillis - lastDotUpdate >= 500) { display.clearDisplay(); + int dotCount = step % 4; String connectingText = "WiFi"; for (int i = 0; i < dotCount; i++) { connectingText += "."; @@ -243,12 +241,7 @@ void connectToWiFi() { display.display(); - dotCount += direction; - if (dotCount >= 3) { - direction = -1; // Start decreasing - } else if (dotCount <= 0) { - direction = 1; // Start increasing - } + step = (step + 1) % 4; lastDotUpdate = currentMillis; } From 4a38e5485ca3fe197f20c23e071624f7d9a415d7 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 8 Dec 2025 08:15:36 -0600 Subject: [PATCH 3/5] Update weather example to new format showing Time, Date, and Weather --- examples/weather/src/main.cpp | 98 +++++++++++++++++++++-------------- 1 file changed, 58 insertions(+), 40 deletions(-) diff --git a/examples/weather/src/main.cpp b/examples/weather/src/main.cpp index f91155f..4a19879 100644 --- a/examples/weather/src/main.cpp +++ b/examples/weather/src/main.cpp @@ -13,10 +13,8 @@ Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1); #define GAME_HEIGHT 128 // Update to your WiFi creds! -//const char* ssid = "Wokwi-GUEST"; -//const char* pass = ""; -const char* ssid = "kame house"; -const char* pass = "spacedicks"; +const char* ssid = "Wokwi-GUEST"; +const char* pass = ""; // Update to your location! // Default for Omaha, NE const float latitude = 41.2565; @@ -67,7 +65,7 @@ int drawCenteredText(String text, int y, int textSize, int maxWidth) { display.getTextBounds(text, 0, 0, &x1, &y1, &w, &h); - // Calculate width using character count for default font (6 pixels per char) + // Calculate width using character count int charWidth = text.length() * 6 * textSize; // Use the larger of the two widths for the wrapping check int checkW = (charWidth > w) ? charWidth : w; @@ -349,6 +347,7 @@ void loop() { // Draw display display.clearDisplay(); + display.drawRoundRect(0, 0, GAME_WIDTH, GAME_HEIGHT, 4, SSD1306_WHITE); // Time on top display.setTextSize(1); @@ -358,50 +357,60 @@ void loop() { String timeLabel = "Time"; display.getTextBounds(timeLabel, 0, 0, &x1, &y1, &w, &h); - int timeLabelX = (GAME_WIDTH - w) / 2; int timeLabelY = 5; - - // Draw border - int timeBoxPadding = 3; - int timeBoxX = 0; + int timeBoxW = 56; + int timeBoxPadding = 2; + int timeBoxX = (GAME_WIDTH - timeBoxW) / 2; int timeBoxY = timeLabelY - timeBoxPadding; - int timeBoxW = GAME_WIDTH; int timeBoxH = h + (timeBoxPadding * 2); - display.drawRoundRect(timeBoxX, timeBoxY, timeBoxW, timeBoxH, 2, SSD1306_WHITE); + display.fillRoundRect(timeBoxX, timeBoxY, timeBoxW, timeBoxH, 2, SSD1306_WHITE); + int timeLabelX = timeBoxX + (timeBoxW - w) / 2; + display.setTextColor(SSD1306_BLACK); display.setCursor(timeLabelX, timeLabelY); display.println(timeLabel); + display.setTextColor(SSD1306_WHITE); - display.setTextSize(1); // Changed from size 2 to size 1 + display.setTextSize(2); display.getTextBounds(currentTime, 0, 0, &x1, &y1, &w, &h); int timeX = (GAME_WIDTH - w) / 2; display.setCursor(timeX, 20); display.println(currentTime); - // Date closer to time display.setTextSize(1); String dateLabel = "Date"; display.getTextBounds(dateLabel, 0, 0, &x1, &y1, &w, &h); - int dateLabelX = (GAME_WIDTH - w) / 2; int dateLabelY = 40; - - // Draw border - int dateBoxPadding = 3; - int dateBoxX = 0; + int dateBoxW = 56; + int dateBoxPadding = 2; + int dateBoxX = (GAME_WIDTH - dateBoxW) / 2; int dateBoxY = dateLabelY - dateBoxPadding; - int dateBoxW = GAME_WIDTH; int dateBoxH = h + (dateBoxPadding * 2); - display.drawRoundRect(dateBoxX, dateBoxY, dateBoxW, dateBoxH, 2, SSD1306_WHITE); + display.fillRoundRect(dateBoxX, dateBoxY, dateBoxW, dateBoxH, 2, SSD1306_WHITE); + int dateLabelX = dateBoxX + (dateBoxW - w) / 2; + display.setTextColor(SSD1306_BLACK); display.setCursor(dateLabelX, dateLabelY); display.println(dateLabel); + display.setTextColor(SSD1306_WHITE); // Get current date struct tm timeinfo; if (getLocalTime(&timeinfo)) { char dateBuffer[20]; - strftime(dateBuffer, sizeof(dateBuffer), "%m/%d/%Y", &timeinfo); + strftime(dateBuffer, sizeof(dateBuffer), "%a %b %d", &timeinfo); String currentDate = String(dateBuffer); + // Remove leading zero from day + int spacePos = currentDate.lastIndexOf(' '); + if (spacePos != -1 && currentDate[spacePos + 1] == '0') { + currentDate.remove(spacePos + 1, 1); + } + // Uppercase only the first letter of the month + int firstSpace = currentDate.indexOf(' '); + int secondSpace = currentDate.indexOf(' ', firstSpace + 1); + if (firstSpace != -1 && secondSpace != -1) { + currentDate[firstSpace + 1] = toupper(currentDate[firstSpace + 1]); + } display.getTextBounds(currentDate, 0, 0, &x1, &y1, &w, &h); int dateX = (GAME_WIDTH - w) / 2; @@ -409,39 +418,48 @@ void loop() { display.println(currentDate); } - // Weather closer to date display.setTextSize(1); String weatherLabel = "Weather"; display.getTextBounds(weatherLabel, 0, 0, &x1, &y1, &w, &h); - int weatherLabelX = (GAME_WIDTH - w) / 2; - int weatherLabelY = 80; - - // Draw rounded box around weather label - int boxPadding = 3; - int boxX = 0; + int weatherLabelY = 68; + int weatherBoxW = 56; + int boxPadding = 2; + int boxX = (GAME_WIDTH - weatherBoxW) / 2; int boxY = weatherLabelY - boxPadding; - int boxW = GAME_WIDTH; int boxH = h + (boxPadding * 2); - display.drawRoundRect(boxX, boxY, boxW, boxH, 2, SSD1306_WHITE); + display.fillRoundRect(boxX, boxY, weatherBoxW, boxH, 2, SSD1306_WHITE); + int weatherLabelX = boxX + (weatherBoxW - w) / 2; + display.setTextColor(SSD1306_BLACK); display.setCursor(weatherLabelX, weatherLabelY); display.println(weatherLabel); + display.setTextColor(SSD1306_WHITE); String desc = weatherDescription.length() == 0 ? "Loading weather..." : weatherDescription; - int weatherHeight = drawCenteredText(desc, 95, 1, GAME_WIDTH); + if (desc.length() > 25) desc = desc.substring(0, 25) + "..."; + int weatherHeight = drawCenteredText(desc, 85, 1, GAME_WIDTH); - String tempStr = (temperature == 0.0 && weatherDescription.length() == 0) ? "" : String((int)temperature) + " F"; + String tempStr = (temperature == 0.0 && weatherDescription.length() == 0) ? "" : String((int)temperature); if (tempStr != "") { - display.setTextSize(1); // Changed from size 2 to size 1 - display.getTextBounds(tempStr, 0, 0, &x1, &y1, &w, &h); - int tempX = (GAME_WIDTH - w) / 2; - int tempY = 93 + weatherHeight + 8; - display.setCursor(tempX, tempY); - display.println(tempStr); + int tempY = 80 + weatherHeight + 8; + if (weatherHeight <= 10) tempY += 6; + display.setTextSize(2); + uint16_t numW, fW, th; + display.getTextBounds(tempStr, 0, 0, &x1, &y1, &numW, &th); + display.setTextSize(1); + display.getTextBounds("F", 0, 0, &x1, &y1, &fW, &th); + int totalW = numW + fW; + int startX = (GAME_WIDTH - totalW) / 2; + display.setTextSize(2); + display.setCursor(startX, tempY); + display.print(tempStr); + display.setTextSize(1); + display.setCursor(startX + numW + 2, tempY + 6); + display.print("F"); display.setTextSize(1); } display.display(); delay(1000); -} +} \ No newline at end of file From e49e6b6a5537e8c96dbe47cdb764db88095741ee Mon Sep 17 00:00:00 2001 From: = Date: Mon, 8 Dec 2025 08:17:27 -0600 Subject: [PATCH 4/5] Resolve merge conflict --- examples/weather/src/main.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/examples/weather/src/main.cpp b/examples/weather/src/main.cpp index 4df3177..19a780e 100644 --- a/examples/weather/src/main.cpp +++ b/examples/weather/src/main.cpp @@ -12,11 +12,8 @@ Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1); #define GAME_WIDTH 64 #define GAME_HEIGHT 128 -<<<<<<< HEAD // Update to your WiFi creds! -======= // Update to your WiFi creds!WiFi ->>>>>>> 244d333fa4bbc19a70c6801e113378fdfa81c1df const char* ssid = "Wokwi-GUEST"; const char* pass = ""; // Update to your location! From a956c31d6dc4b3a842adefa60a31e0dfd44640c1 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 8 Dec 2025 13:38:36 -0600 Subject: [PATCH 5/5] Update documentation and image --- docs/gifs/weather.gif | Bin 3604 -> 6333 bytes examples/weather/README.md | 5 +++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/gifs/weather.gif b/docs/gifs/weather.gif index ac44bb4ea4d24b181111478b1c7dad603c8e13df..d6a16f710e47d887baa3731ab92b10db9512d3d7 100644 GIT binary patch literal 6333 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 literal 3604 zcmV+v4(stpNk%w1VGaTO0dN2SA^8LW00093EC2ui01g5C0f7Jj01ONb|Ns900008` zgpaAq?GK}zwAzca-n{z{hT=$;=82~2%C_zc$MQ_q_KoNI&iDQg3<`(DqVb4KDwoWr z^9hYgr_`$Tip^@b+^+WvJ_^RS|I3TWdi(0&0M3 z+Y7-f0_;01AIv!z8PKr(_ou0hz z0PkKYzuZ3WTfd->-^HKsub-BHq3#KUMKIc!gTxX7aL90qLx>Th|4cMkan!|y8Eb76 z(9t8tSR6yD`e?G;NNg)pdO_(@UCVAWV-Bn-4yVm4FpsUIIIX2lPCYkV1M0J6gQ1&> zHv7p`MYgE)s4le%5UNrfQ@3uzx({qYuUW~~=nC?g(+o$&o-&KTEtj<<(_V!;11&VV zEb%^+8%(HQm4j&(KBv`gTsG{^G>)@aahk`b6h=0a^RQ9C3Kc)~i>wXh#Bd^CZhI%J zeCw?X#q<(oB+|J^lW{luN~Zr|U2@cj~9 zS>x5$*?hxo#vg4#Z3kh5&6O40e(_n>79OB+8vnS8?{j<9d`t3H{o9% z4%iBa0g4FVdb*`(+ln5lXrCJZ(ir256d0Esi7vL-V>o(cm?UHVb!gpiL`G<2RN!?- zABT4t`JNb3s#oNHPY$V|kVjTISe9g(nZtF4jVPv;Fyc7pnPaxbrEqL=^`(py))?lS zc}Q zisF$@T1e@Zq+W@t4VvzB6Q+OSYRRp<^3-RozdqsX5wKJgYpfK=CJV)~%sS!hvrPX) zE3FdLR*S^7*czejwnTL6tq|ab3&gnO`e5$4Jfy3x4(zter>=;?yTrTno)zr9&E~5w zwEXrv?Y{t9EAYVC7HlxL2q*mQ!VHV+@WbUsoL-LD;>Rfn5WTuB#&&+NF{t5sY_F@H zR!cI)YhLK_I}0)jGMJ#f{7uU8#tGz%%-Srh&K0vvvo<`d+Vie6a)+GAINAm2tgMPq0~f=4P}T(JAI#{U32_3p<&;svC5$?EvnI4r@iXh zN?VGhi6#0-cFgh~S2u}-_ZVs6cZR6Apg{(nbK;BBUA5$YTkhhDb7TE;lvJ939C_Af zS1$U=mGcPep&u5#`D?3Ro%z*bldh+TdRxr+TdY%Vwb^N^Eyuh}{(O7WxsQ)#MhAjBhjxl_q5$WhYEaK6LZ>-=RSzyLLrYensl%pN{SV#-%F_B-K zBO}X3$VURwk&<-XA}c8sN@6mPk=!H_HR(x3b~2Pcyd)`0Im%N?@qemJB`aGA$uYum zm6n{PDPw6%T#7N5yX++|(f=sRU%nEU!X&0Ii|II5B2$#XT&DhL>CEaiGnx_H6g87M z%|>=Gg_4L@BI?-3mURkj-DHF!zzNMsZqqmAv{fU}dCN&e4{d+o;yIUiNIrVU5%&Bh zKEnw`azc%t-fZX80D8w6PS2m_Wa#+vNdp=-2@w4>XgWEWNx*4yoidPULoa$z9=cJS zZcFGx5sJu8fpDY&)n7~FIZWM&G)N@dvgLb*w+tDO%5((y@PrwJuXdYgY|f)Trtbi+#;2`0n}|vkq3UVjXB*`FcX2)>EA* zE$coXE7ZuAk)o8{t4akLQq5x34=sovT>olQ+--KUD=;l)5ocG`jYTt|Cq%xMl3&qNEUGQMQ&R46BeJy|kj9Z8;7{jN{uzAx9;>Ak1 zxDB?ji8-v_4bN4%^+j=vuiM%Z8`y>s2JnpqoKXbYAqX!f@rYTxs5}sP$6h@$WxY4# ziFr7#`)#t2>#*c0^HIj3Wu20x{NDkuH^@-NGMKkJW^4ks%+2dTk&DaaXa+KrbM|DN zH+g4F=DCu4mSmqF`DaE3I+24eWT6Fl=szZUkBi1*qv`nQI7YgSlU8G;&v2VyT;Xcxol4`iQF@Vyl7pYKg`AVY6mftrK=@gXMZ)y9QXV`}J#m z1^ZsZrdP4yb?kN}TV2aOSF_3W?2(mMn9=rDwL3QLUtarS+2-Z8QP%BTep_1Oepb16 z7H(UfJ6Y>CR=bDw?q9{5SM$bIy?1hNo8&tt`*umcQ~L66lLR~@1Mf(|Gji~XBs?Mu zXGp^j@^FDfydM+CN5$uHad%`q9UCV{$7`1ES$Y>V z+_DTyGk4$P!m8|rG6I<}YY z<_gcOKuD`qT|Jjs3%oGuf2Ce#{kDQR@v9piy<4;*-YNss)r^ zoJ5GiW2OB=Vn!4H3Uxe*4E{ldKOgWAzjKQ$J}Heq-Q#t5Rk){_nPPvY?+%ao$7+7U zq?dB#LB|UbzA*Axu5#zqoW0cV@aOGQ{}|RKdwRfI-s!7#y@GR}CEd&9_NxW{15uCW zxf36&#)rJ{H8ObyDHnamr`3g(75&dl{~p4({+n&@dl7GcfYmST=$#8>+?UjJ-8X;o z`5yjj2EO$22m0pg4@bG<-)>;ve+@nFfAgn*0a$dWM}RTsGRud52>5ggSbq%o6%HtW z5SS0yr)U<)Xc*{d8VG3|C}|!@X&`86B8X`usA(q1X(;GvDhO&UC~7W9YL4`MJcc9$ z!(A5gJ`#fk05~mUwDPq(`Gg(g*3Q?XSjoF z2wiy9dPDVOekWjhm|yMHhoVJb%9Bl@$A^S?W`;;%TNi&gcx99Le%M8Z=#_Z-Cx}Iu zWLubHHTPDTr!t!uV$@ZLvUP}nn23_dX06D3>vxIw^@axMgr>-djfIJiNQ;~YhPP;o zxG0OM*mn}>c8e%hLzjz2RzJj8WX9y8i{SW;(6x>$Igbu$j@Fotdst&L8H%Fkk}+9~ zL1~HF2!DuZlLwZQg9wZe_KF%Zi}nbSLWz!3NmP|MD_3cSAQ_dM7>*PPlF~?aVEK?@ zsgz{dlU-Srx8;%LIE`D0jSJ~wZFoB}29J~&mpVC=**KS2$(DABlc(ZfdMS#`2$Oz^ zmN~aVFScAK>564}eoM)ffH|09nP7&gl|{ggaw(WTSw5e6n4T$OF&UaNNt*OEg&KL5 zu$gsc_FbS@nt^zP>L{C%m6Bw+oBp_7A{mV{zd2h1Ih?lXn!%ZyI!T-;)|?61kY3n) zy$OrY$y~;%o5(pxs~Cj(=$qC_Wl)%%3W7Azto}p)wqj;X*nVyX(k>l5o z{P&Ls2!p1UpQyH_Wp~$wO%GROG_My%eqR=*?O4p49Sb&6yqWSlcE1II5xuO)<3o;50B|3G)NKG^< z5;!VnHhOg@il02{Ogf4XKFV!Ex=ch$7>0G3F<*E`y6~by`ch4*pHD@l{As0T$!A)M zj}OSD0C`P3X#h|PosnRsXNsngaGhz&rfuq`W@u$^DyMTwr`Q>%b&98Xs;4f>r+wsD)~%hl;3)s;F8}0029