Merge branch 'feat-weather' of https://github.com/jpirnay/crosspoint-reader into mybuild

This commit is contained in:
jpirnay
2026-04-02 18:37:55 +02:00
35 changed files with 3284 additions and 28 deletions
+4
View File
@@ -177,3 +177,7 @@ CrossPoint Reader is **not affiliated with Xteink or any manufacturer of the X4
Huge shoutout to [**diy-esp32-epub-reader** by atomic14](https://github.com/atomic14/diy-esp32-epub-reader), which was a project I took a lot of inspiration from as I
was making CrossPoint.
## Third-Party Attribution
- Weather icon artwork used by the Weather activity is adapted from [weather-icons by Erik Flowers](https://github.com/erikflowers/weather-icons).
+18
View File
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
<path d="M3.89,17.6c0-0.99,0.31-1.88,0.93-2.65s1.41-1.27,2.38-1.49c0.26-1.17,0.85-2.14,1.78-2.88c0.93-0.75,2-1.12,3.22-1.12
c1.18,0,2.24,0.36,3.16,1.09c0.93,0.73,1.53,1.66,1.8,2.8h0.27c1.18,0,2.18,0.41,3.01,1.24s1.25,1.83,1.25,3
c0,1.18-0.42,2.18-1.25,3.01s-1.83,1.25-3.01,1.25H8.16c-0.58,0-1.13-0.11-1.65-0.34S5.52,21,5.14,20.62
c-0.38-0.38-0.68-0.84-0.91-1.36S3.89,18.17,3.89,17.6z M5.34,17.6c0,0.76,0.28,1.42,0.82,1.96s1.21,0.82,1.99,0.82h9.28
c0.77,0,1.44-0.27,1.99-0.82c0.55-0.55,0.83-1.2,0.83-1.96c0-0.76-0.27-1.42-0.83-1.96c-0.55-0.54-1.21-0.82-1.99-0.82h-1.39
c-0.1,0-0.15-0.05-0.15-0.15l-0.07-0.49c-0.1-0.94-0.5-1.73-1.19-2.35s-1.51-0.93-2.45-0.93c-0.94,0-1.76,0.31-2.46,0.94
c-0.7,0.62-1.09,1.41-1.18,2.34l-0.07,0.42c0,0.1-0.05,0.15-0.16,0.15l-0.45,0.07c-0.72,0.06-1.32,0.36-1.81,0.89
C5.59,16.24,5.34,16.87,5.34,17.6z M14.19,8.88c-0.1,0.09-0.08,0.16,0.07,0.21c0.43,0.19,0.79,0.37,1.08,0.55
c0.11,0.03,0.19,0.02,0.22-0.03c0.61-0.57,1.31-0.86,2.12-0.86c0.81,0,1.5,0.27,2.1,0.81c0.59,0.54,0.92,1.21,0.99,2l0.09,0.64h1.42
c0.65,0,1.21,0.23,1.68,0.7c0.47,0.47,0.7,1.02,0.7,1.66c0,0.6-0.21,1.12-0.62,1.57s-0.92,0.7-1.53,0.77c-0.1,0-0.15,0.05-0.15,0.16
v1.13c0,0.11,0.05,0.16,0.15,0.16c1.01-0.06,1.86-0.46,2.55-1.19s1.04-1.6,1.04-2.6c0-1.06-0.37-1.96-1.12-2.7
c-0.75-0.75-1.65-1.12-2.7-1.12h-0.15c-0.26-1-0.81-1.82-1.65-2.47c-0.83-0.65-1.77-0.97-2.8-0.97C16.28,7.29,15.11,7.82,14.19,8.88
z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
<path d="M1.56,16.9c0,0.9,0.22,1.73,0.66,2.49s1.04,1.36,1.8,1.8c0.76,0.44,1.58,0.66,2.47,0.66h10.83c0.89,0,1.72-0.22,2.48-0.66
c0.76-0.44,1.37-1.04,1.81-1.8c0.44-0.76,0.67-1.59,0.67-2.49c0-0.66-0.14-1.33-0.42-2C22.62,13.98,23,12.87,23,11.6
c0-0.71-0.14-1.39-0.41-2.04c-0.27-0.65-0.65-1.2-1.12-1.67C21,7.42,20.45,7.04,19.8,6.77c-0.65-0.28-1.33-0.41-2.04-0.41
c-1.48,0-2.77,0.58-3.88,1.74c-0.77-0.44-1.67-0.66-2.7-0.66c-1.41,0-2.65,0.44-3.73,1.31c-1.08,0.87-1.78,1.99-2.08,3.35
c-1.12,0.26-2.03,0.83-2.74,1.73S1.56,15.75,1.56,16.9z M3.27,16.9c0-0.84,0.28-1.56,0.84-2.17c0.56-0.61,1.26-0.96,2.1-1.06
l0.5-0.03c0.12,0,0.19-0.06,0.19-0.18l0.07-0.54c0.14-1.08,0.61-1.99,1.41-2.71c0.8-0.73,1.74-1.09,2.81-1.09
c1.1,0,2.06,0.37,2.87,1.1c0.82,0.73,1.27,1.63,1.37,2.71l0.07,0.58c0.02,0.11,0.09,0.17,0.21,0.17h1.61c0.88,0,1.64,0.32,2.28,0.96
c0.64,0.64,0.96,1.39,0.96,2.27c0,0.91-0.32,1.68-0.95,2.32c-0.63,0.64-1.4,0.96-2.28,0.96H6.49c-0.88,0-1.63-0.32-2.27-0.97
C3.59,18.57,3.27,17.8,3.27,16.9z M9.97,4.63c0,0.24,0.08,0.45,0.24,0.63l0.66,0.64c0.25,0.19,0.46,0.27,0.64,0.25
c0.21,0,0.39-0.09,0.55-0.26s0.24-0.38,0.24-0.62c0-0.24-0.09-0.44-0.26-0.59l-0.59-0.66c-0.18-0.16-0.38-0.24-0.61-0.24
c-0.24,0-0.45,0.08-0.62,0.25C10.05,4.19,9.97,4.39,9.97,4.63z M15.31,9.06c0.69-0.67,1.51-1,2.45-1c0.99,0,1.83,0.34,2.52,1.03
c0.69,0.69,1.04,1.52,1.04,2.51c0,0.62-0.17,1.24-0.51,1.84C19.84,12.48,18.68,12,17.32,12H17C16.75,10.91,16.19,9.93,15.31,9.06z
M16.94,3.78c0,0.26,0.08,0.46,0.23,0.62s0.35,0.23,0.59,0.23c0.26,0,0.46-0.08,0.62-0.23c0.16-0.16,0.23-0.36,0.23-0.62V1.73
c0-0.24-0.08-0.43-0.24-0.59s-0.36-0.23-0.61-0.23c-0.24,0-0.43,0.08-0.59,0.23s-0.23,0.35-0.23,0.59V3.78z M22.46,6.07
c0,0.26,0.07,0.46,0.22,0.62c0.21,0.16,0.42,0.24,0.62,0.24c0.18,0,0.38-0.08,0.59-0.24l1.43-1.43c0.16-0.18,0.24-0.39,0.24-0.64
c0-0.24-0.08-0.44-0.24-0.6c-0.16-0.16-0.36-0.24-0.59-0.24c-0.24,0-0.43,0.08-0.58,0.24l-1.47,1.43
C22.53,5.64,22.46,5.84,22.46,6.07z M23.25,17.91c0,0.24,0.08,0.45,0.25,0.63l0.65,0.63c0.15,0.16,0.34,0.24,0.58,0.24
s0.44-0.08,0.6-0.25c0.16-0.17,0.24-0.37,0.24-0.62c0-0.22-0.08-0.42-0.24-0.58l-0.65-0.65c-0.16-0.16-0.35-0.24-0.57-0.24
c-0.24,0-0.44,0.08-0.6,0.24C23.34,17.47,23.25,17.67,23.25,17.91z M24.72,11.6c0,0.23,0.09,0.42,0.26,0.58
c0.16,0.16,0.37,0.24,0.61,0.24h2.04c0.23,0,0.42-0.08,0.58-0.23s0.23-0.35,0.23-0.59c0-0.24-0.08-0.44-0.23-0.6
s-0.35-0.25-0.58-0.25h-2.04c-0.24,0-0.44,0.08-0.61,0.25C24.8,11.17,24.72,11.37,24.72,11.6z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

+27
View File
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
<path d="M4.37,14.62c0-0.24,0.08-0.45,0.25-0.62c0.17-0.16,0.38-0.24,0.6-0.24h2.04c0.23,0,0.42,0.08,0.58,0.25
c0.15,0.17,0.23,0.37,0.23,0.61S8,15.06,7.85,15.23c-0.15,0.17-0.35,0.25-0.58,0.25H5.23c-0.23,0-0.43-0.08-0.6-0.25
C4.46,15.06,4.37,14.86,4.37,14.62z M7.23,21.55c0-0.23,0.08-0.43,0.23-0.61l1.47-1.43c0.15-0.16,0.35-0.23,0.59-0.23
c0.24,0,0.44,0.08,0.6,0.23s0.24,0.34,0.24,0.57c0,0.24-0.08,0.46-0.24,0.64L8.7,22.14c-0.41,0.32-0.82,0.32-1.23,0
C7.31,21.98,7.23,21.78,7.23,21.55z M7.23,7.71c0-0.23,0.08-0.43,0.23-0.61C7.66,6.93,7.87,6.85,8.1,6.85
c0.22,0,0.42,0.08,0.59,0.24l1.43,1.47c0.16,0.15,0.24,0.35,0.24,0.59c0,0.24-0.08,0.44-0.24,0.6s-0.36,0.24-0.6,0.24
c-0.24,0-0.44-0.08-0.59-0.24L7.47,8.32C7.31,8.16,7.23,7.95,7.23,7.71z M9.78,14.62c0-0.93,0.23-1.8,0.7-2.6s1.1-1.44,1.91-1.91
s1.67-0.7,2.6-0.7c0.7,0,1.37,0.14,2.02,0.42c0.64,0.28,1.2,0.65,1.66,1.12c0.47,0.47,0.84,1.02,1.11,1.66
c0.27,0.64,0.41,1.32,0.41,2.02c0,0.94-0.23,1.81-0.7,2.61c-0.47,0.8-1.1,1.43-1.9,1.9c-0.8,0.47-1.67,0.7-2.61,0.7
s-1.81-0.23-2.61-0.7c-0.8-0.47-1.43-1.1-1.9-1.9C10.02,16.43,9.78,15.56,9.78,14.62z M11.48,14.62c0,0.98,0.34,1.81,1.03,2.5
c0.68,0.69,1.51,1.04,2.49,1.04s1.81-0.35,2.5-1.04s1.04-1.52,1.04-2.5c0-0.96-0.35-1.78-1.04-2.47c-0.69-0.68-1.52-1.02-2.5-1.02
c-0.97,0-1.8,0.34-2.48,1.02C11.82,12.84,11.48,13.66,11.48,14.62z M14.14,22.4c0-0.24,0.08-0.44,0.25-0.6s0.37-0.24,0.6-0.24
c0.24,0,0.45,0.08,0.61,0.24s0.24,0.36,0.24,0.6v1.99c0,0.24-0.08,0.45-0.25,0.62c-0.17,0.17-0.37,0.25-0.6,0.25
s-0.44-0.08-0.6-0.25c-0.17-0.17-0.25-0.38-0.25-0.62V22.4z M14.14,6.9V4.86c0-0.23,0.08-0.43,0.25-0.6C14.56,4.09,14.76,4,15,4
s0.43,0.08,0.6,0.25c0.17,0.17,0.25,0.37,0.25,0.6V6.9c0,0.23-0.08,0.42-0.25,0.58S15.23,7.71,15,7.71s-0.44-0.08-0.6-0.23
S14.14,7.13,14.14,6.9z M19.66,20.08c0-0.23,0.08-0.42,0.23-0.56c0.15-0.16,0.34-0.23,0.56-0.23c0.24,0,0.44,0.08,0.6,0.23
l1.46,1.43c0.16,0.17,0.24,0.38,0.24,0.61c0,0.23-0.08,0.43-0.24,0.59c-0.4,0.31-0.8,0.31-1.2,0l-1.42-1.42
C19.74,20.55,19.66,20.34,19.66,20.08z M19.66,9.16c0-0.25,0.08-0.45,0.23-0.59l1.42-1.47c0.17-0.16,0.37-0.24,0.59-0.24
c0.24,0,0.44,0.08,0.6,0.25c0.17,0.17,0.25,0.37,0.25,0.6c0,0.25-0.08,0.46-0.24,0.62l-1.46,1.43c-0.18,0.16-0.38,0.24-0.6,0.24
c-0.23,0-0.41-0.08-0.56-0.24S19.66,9.4,19.66,9.16z M21.92,14.62c0-0.24,0.08-0.44,0.24-0.62c0.16-0.16,0.35-0.24,0.57-0.24h2.02
c0.23,0,0.43,0.09,0.6,0.26c0.17,0.17,0.26,0.37,0.26,0.6s-0.09,0.43-0.26,0.6c-0.17,0.17-0.37,0.25-0.6,0.25h-2.02
c-0.23,0-0.43-0.08-0.58-0.25S21.92,14.86,21.92,14.62z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

+18
View File
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
<path d="M2.62,21.05c0-0.24,0.08-0.45,0.25-0.61c0.17-0.16,0.38-0.24,0.63-0.24h18.67c0.25,0,0.45,0.08,0.61,0.24
c0.16,0.16,0.24,0.36,0.24,0.61c0,0.23-0.08,0.43-0.25,0.58c-0.17,0.16-0.37,0.23-0.6,0.23H3.5c-0.25,0-0.46-0.08-0.63-0.23
C2.7,21.47,2.62,21.28,2.62,21.05z M5.24,17.91c0-0.24,0.09-0.44,0.26-0.6c0.15-0.15,0.35-0.23,0.59-0.23h18.67
c0.23,0,0.42,0.08,0.58,0.24c0.16,0.16,0.23,0.35,0.23,0.59c0,0.24-0.08,0.44-0.23,0.6c-0.16,0.17-0.35,0.25-0.58,0.25H6.09
c-0.24,0-0.44-0.08-0.6-0.25C5.32,18.34,5.24,18.14,5.24,17.91z M5.37,15.52c0,0.09,0.05,0.13,0.15,0.13h1.43
c0.06,0,0.13-0.05,0.2-0.16c0.24-0.52,0.59-0.94,1.06-1.27c0.47-0.33,0.99-0.52,1.55-0.56l0.55-0.07c0.11,0,0.17-0.06,0.17-0.18
l0.07-0.5c0.11-1.08,0.56-1.98,1.37-2.7c0.81-0.72,1.76-1.08,2.85-1.08c1.08,0,2.02,0.36,2.83,1.07c0.8,0.71,1.26,1.61,1.37,2.68
l0.08,0.57c0,0.11,0.07,0.17,0.2,0.17h1.59c0.64,0,1.23,0.17,1.76,0.52s0.92,0.8,1.18,1.37c0.07,0.11,0.14,0.16,0.21,0.16h1.43
c0.12,0,0.17-0.07,0.14-0.23c-0.29-1.02-0.88-1.86-1.74-2.51c-0.87-0.65-1.86-0.97-2.97-0.97h-0.32c-0.33-1.33-1.03-2.42-2.1-3.27
s-2.28-1.27-3.65-1.27c-1.4,0-2.64,0.44-3.73,1.32s-1.78,2-2.09,3.36c-0.85,0.2-1.6,0.6-2.24,1.21c-0.64,0.61-1.09,1.33-1.34,2.18
v-0.04C5.37,15.45,5.37,15.48,5.37,15.52z M6.98,24.11c0-0.24,0.09-0.43,0.26-0.59c0.15-0.15,0.35-0.23,0.6-0.23h18.68
c0.24,0,0.44,0.08,0.6,0.23c0.17,0.16,0.25,0.35,0.25,0.58c0,0.24-0.08,0.44-0.25,0.61c-0.17,0.17-0.37,0.25-0.6,0.25H7.84
c-0.23,0-0.43-0.09-0.6-0.26C7.07,24.55,6.98,24.34,6.98,24.11z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
<path d="M4.14,16.9c0-1.16,0.35-2.18,1.06-3.08s1.62-1.47,2.74-1.72c0.23-1.03,0.7-1.93,1.4-2.7c0.7-0.77,1.55-1.32,2.53-1.65
c0.62-0.21,1.26-0.32,1.93-0.32c0.81,0,1.6,0.16,2.35,0.48c0.28-0.47,0.61-0.88,0.99-1.22c0.38-0.34,0.77-0.61,1.17-0.79
c0.4-0.18,0.8-0.32,1.18-0.41s0.76-0.13,1.12-0.13c0.38,0,0.79,0.05,1.23,0.16l0.82,0.25c0.14,0.06,0.18,0.13,0.14,0.22l-0.14,0.6
c-0.07,0.31-0.1,0.6-0.1,0.86c0,0.31,0.05,0.63,0.15,0.95c0.1,0.32,0.24,0.63,0.44,0.94c0.19,0.31,0.46,0.58,0.8,0.83
c0.34,0.25,0.72,0.44,1.15,0.57l0.62,0.22c0.1,0.03,0.15,0.08,0.15,0.16c0,0.02-0.01,0.04-0.02,0.07l-0.18,0.67
c-0.27,1.08-0.78,1.93-1.5,2.57c0.4,0.7,0.62,1.45,0.65,2.24c0.01,0.05,0.01,0.12,0.01,0.23c0,0.89-0.22,1.72-0.67,2.48
c-0.44,0.76-1.05,1.36-1.8,1.8c-0.76,0.44-1.59,0.67-2.48,0.67H9.07c-0.89,0-1.72-0.22-2.48-0.67s-1.35-1.05-1.79-1.8
S4.14,17.8,4.14,16.9z M5.85,16.9c0,0.89,0.32,1.66,0.96,2.31c0.64,0.65,1.39,0.98,2.26,0.98h10.81c0.89,0,1.65-0.32,2.28-0.97
s0.95-1.42,0.95-2.32c0-0.88-0.32-1.63-0.96-2.26c-0.64-0.63-1.4-0.95-2.28-0.95h-1.78l-0.1-0.75c-0.1-1.01-0.52-1.88-1.26-2.59
s-1.62-1.11-2.63-1.2c-0.03,0-0.08,0-0.15-0.01c-0.07-0.01-0.11-0.01-0.15-0.01c-0.51,0-1.02,0.1-1.54,0.29V9.4
c-0.73,0.28-1.35,0.74-1.84,1.37c-0.5,0.63-0.8,1.35-0.9,2.17l-0.07,0.72l-0.68,0.03c-0.84,0.1-1.54,0.45-2.1,1.06
S5.85,16.07,5.85,16.9z M17.6,8.79c1.06,0.91,1.72,1.97,1.97,3.18h0.32c1.24,0,2.3,0.39,3.17,1.18c0.33-0.31,0.58-0.67,0.76-1.07
c-0.91-0.43-1.63-1.09-2.16-1.97c-0.52-0.88-0.79-1.81-0.79-2.78V7.09c-0.05-0.01-0.13-0.01-0.24-0.01
c-0.58-0.01-1.15,0.13-1.7,0.44C18.38,7.82,17.93,8.24,17.6,8.79z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
<path d="M7.91,14.48c0-0.96,0.19-1.87,0.56-2.75s0.88-1.63,1.51-2.26c0.63-0.63,1.39-1.14,2.27-1.52c0.88-0.38,1.8-0.57,2.75-0.57
h1.14c0.16,0.04,0.23,0.14,0.23,0.28l0.05,0.88c0.04,1.27,0.49,2.35,1.37,3.24c0.88,0.89,1.94,1.37,3.19,1.42l0.82,0.07
c0.16,0,0.24,0.08,0.24,0.23v0.98c0.01,1.28-0.3,2.47-0.93,3.56c-0.63,1.09-1.48,1.95-2.57,2.59c-1.08,0.63-2.27,0.95-3.55,0.95
c-0.97,0-1.9-0.19-2.78-0.56s-1.63-0.88-2.26-1.51c-0.63-0.63-1.13-1.39-1.5-2.26C8.1,16.37,7.91,15.45,7.91,14.48z M9.74,14.48
c0,0.76,0.15,1.48,0.45,2.16c0.3,0.67,0.7,1.24,1.19,1.7c0.49,0.46,1.05,0.82,1.69,1.08c0.63,0.27,1.28,0.4,1.94,0.4
c0.58,0,1.17-0.11,1.76-0.34c0.59-0.23,1.14-0.55,1.65-0.96c0.51-0.41,0.94-0.93,1.31-1.57c0.37-0.64,0.6-1.33,0.71-2.09
c-1.63-0.34-2.94-1.04-3.92-2.1s-1.55-2.3-1.7-3.74C13.86,9.08,13,9.37,12.21,9.9c-0.78,0.53-1.39,1.2-1.82,2.02
C9.96,12.74,9.74,13.59,9.74,14.48z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
<path d="M4.64,16.91c0-1.15,0.36-2.17,1.08-3.07c0.72-0.9,1.63-1.47,2.73-1.73c0.31-1.36,1.02-2.48,2.11-3.36s2.34-1.31,3.75-1.31
c1.38,0,2.6,0.43,3.68,1.28c1.08,0.85,1.78,1.95,2.1,3.29h0.32c0.89,0,1.72,0.22,2.48,0.65s1.37,1.03,1.81,1.78
c0.44,0.75,0.67,1.58,0.67,2.47c0,0.88-0.21,1.69-0.63,2.44c-0.42,0.75-1,1.35-1.73,1.8c-0.73,0.45-1.53,0.69-2.4,0.71
c-0.13,0-0.2-0.06-0.2-0.17v-1.33c0-0.12,0.07-0.18,0.2-0.18c0.85-0.04,1.58-0.38,2.18-1.02s0.9-1.39,0.9-2.26s-0.33-1.62-0.98-2.26
s-1.42-0.96-2.31-0.96h-1.61c-0.12,0-0.18-0.06-0.18-0.17l-0.08-0.58c-0.11-1.08-0.58-1.99-1.39-2.71
c-0.82-0.73-1.76-1.09-2.85-1.09c-1.09,0-2.05,0.36-2.85,1.09c-0.81,0.73-1.26,1.63-1.36,2.71l-0.07,0.53c0,0.12-0.07,0.19-0.2,0.19
l-0.53,0.03c-0.83,0.1-1.53,0.46-2.1,1.07s-0.85,1.33-0.85,2.16c0,0.87,0.3,1.62,0.9,2.26s1.33,0.98,2.18,1.02
c0.11,0,0.17,0.06,0.17,0.18v1.33c0,0.11-0.06,0.17-0.17,0.17c-1.34-0.06-2.47-0.57-3.4-1.53S4.64,18.24,4.64,16.91z M9.99,23.6
c0-0.04,0.01-0.11,0.04-0.2l1.63-5.77c0.06-0.19,0.17-0.34,0.32-0.44c0.15-0.1,0.31-0.15,0.46-0.15c0.07,0,0.15,0.01,0.24,0.03
c0.24,0.04,0.42,0.17,0.54,0.37c0.12,0.2,0.15,0.42,0.08,0.67l-1.63,5.73c-0.12,0.43-0.4,0.64-0.82,0.64
c-0.04,0-0.07-0.01-0.11-0.02c-0.06-0.02-0.09-0.03-0.1-0.03c-0.22-0.06-0.38-0.17-0.49-0.33C10.04,23.93,9.99,23.77,9.99,23.6z
M12.61,26.41l2.44-8.77c0.04-0.19,0.14-0.34,0.3-0.44c0.16-0.1,0.32-0.15,0.49-0.15c0.09,0,0.18,0.01,0.27,0.03
c0.22,0.06,0.38,0.19,0.49,0.39c0.11,0.2,0.13,0.41,0.07,0.64l-2.43,8.78c-0.04,0.17-0.13,0.31-0.29,0.43
c-0.16,0.12-0.32,0.18-0.51,0.18c-0.09,0-0.18-0.02-0.25-0.05c-0.2-0.05-0.37-0.18-0.52-0.39C12.56,26.88,12.54,26.67,12.61,26.41z
M16.74,23.62c0-0.04,0.01-0.11,0.04-0.23l1.63-5.77c0.06-0.19,0.16-0.34,0.3-0.44c0.15-0.1,0.3-0.15,0.46-0.15
c0.08,0,0.17,0.01,0.26,0.03c0.21,0.06,0.36,0.16,0.46,0.31c0.1,0.15,0.15,0.31,0.15,0.47c0,0.03-0.01,0.08-0.02,0.14
s-0.02,0.1-0.02,0.12l-1.63,5.73c-0.04,0.19-0.13,0.35-0.28,0.46s-0.32,0.17-0.51,0.17l-0.24-0.05c-0.2-0.06-0.35-0.16-0.46-0.32
C16.79,23.94,16.74,23.78,16.74,23.62z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

+27
View File
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
<path d="M4.64,16.95c0-1.16,0.35-2.18,1.06-3.08s1.62-1.48,2.74-1.76c0.31-1.36,1.01-2.48,2.1-3.36s2.34-1.31,3.75-1.31
c1.38,0,2.6,0.43,3.68,1.28c1.08,0.85,1.78,1.95,2.1,3.29h0.32c0.89,0,1.72,0.22,2.48,0.66c0.76,0.44,1.37,1.04,1.81,1.8
c0.44,0.76,0.67,1.59,0.67,2.48c0,1.32-0.46,2.47-1.39,3.42c-0.92,0.96-2.05,1.46-3.38,1.5c-0.13,0-0.2-0.06-0.2-0.17v-1.33
c0-0.12,0.07-0.18,0.2-0.18c0.85-0.04,1.58-0.38,2.18-1.02s0.9-1.38,0.9-2.23c0-0.89-0.32-1.65-0.97-2.3s-1.42-0.97-2.32-0.97h-1.61
c-0.12,0-0.18-0.06-0.18-0.17l-0.08-0.58c-0.11-1.08-0.58-1.99-1.39-2.72c-0.82-0.73-1.76-1.1-2.85-1.1c-1.1,0-2.05,0.37-2.86,1.11
c-0.81,0.74-1.27,1.65-1.37,2.75l-0.06,0.5c0,0.12-0.07,0.19-0.2,0.19l-0.53,0.07c-0.83,0.07-1.53,0.41-2.1,1.04
s-0.85,1.35-0.85,2.19c0,0.85,0.3,1.59,0.9,2.23s1.33,0.97,2.18,1.02c0.11,0,0.17,0.06,0.17,0.18v1.33c0,0.11-0.06,0.17-0.17,0.17
c-1.34-0.04-2.47-0.54-3.4-1.5C5.1,19.42,4.64,18.27,4.64,16.95z M11,21.02c0-0.22,0.08-0.42,0.24-0.58
c0.16-0.16,0.35-0.24,0.59-0.24c0.23,0,0.43,0.08,0.59,0.24c0.16,0.16,0.24,0.36,0.24,0.58c0,0.24-0.08,0.44-0.24,0.6
c-0.16,0.17-0.35,0.25-0.59,0.25c-0.23,0-0.43-0.08-0.59-0.25C11.08,21.46,11,21.26,11,21.02z M11,24.65c0-0.24,0.08-0.44,0.24-0.6
c0.16-0.15,0.35-0.23,0.58-0.23c0.23,0,0.43,0.08,0.59,0.23c0.16,0.16,0.24,0.35,0.24,0.59c0,0.24-0.08,0.43-0.24,0.59
c-0.16,0.16-0.35,0.23-0.59,0.23c-0.23,0-0.43-0.08-0.59-0.23C11.08,25.08,11,24.88,11,24.65z M14.19,22.95
c0-0.23,0.08-0.44,0.25-0.62c0.16-0.16,0.35-0.24,0.57-0.24c0.23,0,0.43,0.09,0.6,0.26c0.17,0.17,0.26,0.37,0.26,0.6
c0,0.23-0.08,0.43-0.25,0.6c-0.17,0.17-0.37,0.25-0.61,0.25c-0.23,0-0.42-0.08-0.58-0.25S14.19,23.18,14.19,22.95z M14.19,19.33
c0-0.23,0.08-0.43,0.25-0.6c0.18-0.16,0.37-0.24,0.57-0.24c0.24,0,0.44,0.08,0.61,0.25c0.17,0.17,0.25,0.36,0.25,0.6
c0,0.23-0.08,0.43-0.25,0.59c-0.17,0.16-0.37,0.24-0.61,0.24c-0.23,0-0.42-0.08-0.58-0.24C14.27,19.76,14.19,19.56,14.19,19.33z
M14.19,26.61c0-0.23,0.08-0.43,0.25-0.61c0.16-0.16,0.35-0.24,0.57-0.24c0.24,0,0.44,0.08,0.61,0.25c0.17,0.17,0.25,0.37,0.25,0.6
s-0.08,0.43-0.25,0.59c-0.17,0.16-0.37,0.24-0.61,0.24c-0.23,0-0.42-0.08-0.58-0.24C14.27,27.03,14.19,26.84,14.19,26.61z
M17.41,21.02c0-0.22,0.08-0.41,0.25-0.58c0.17-0.17,0.37-0.25,0.6-0.25c0.23,0,0.43,0.08,0.59,0.24c0.16,0.16,0.24,0.36,0.24,0.58
c0,0.24-0.08,0.44-0.24,0.6c-0.16,0.17-0.35,0.25-0.59,0.25c-0.24,0-0.44-0.08-0.6-0.25C17.5,21.45,17.41,21.25,17.41,21.02z
M17.41,24.65c0-0.22,0.08-0.42,0.25-0.6c0.16-0.15,0.36-0.23,0.6-0.23c0.24,0,0.43,0.08,0.59,0.23s0.23,0.35,0.23,0.59
c0,0.24-0.08,0.43-0.23,0.59c-0.16,0.16-0.35,0.23-0.59,0.23c-0.24,0-0.44-0.08-0.6-0.24C17.5,25.07,17.41,24.88,17.41,24.65z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

+20
View File
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
<path d="M4.64,16.91c0-1.15,0.36-2.17,1.08-3.07c0.72-0.9,1.63-1.47,2.73-1.73c0.31-1.36,1.01-2.48,2.1-3.35s2.35-1.31,3.76-1.31
c1.38,0,2.6,0.43,3.68,1.27c1.07,0.85,1.78,1.94,2.11,3.28h0.31c0.89,0,1.72,0.22,2.48,0.65s1.37,1.03,1.81,1.78
c0.44,0.75,0.67,1.58,0.67,2.47c0,1.34-0.46,2.49-1.38,3.45s-2.05,1.47-3.38,1.51c-0.13,0-0.2-0.06-0.2-0.17v-1.33
c0-0.12,0.07-0.18,0.2-0.18c0.86-0.04,1.58-0.38,2.18-1.02s0.9-1.39,0.9-2.26s-0.32-1.62-0.98-2.26c-0.65-0.64-1.42-0.96-2.31-0.96
h-1.6c-0.12,0-0.19-0.06-0.19-0.17l-0.07-0.58c-0.11-1.07-0.57-1.98-1.38-2.71c-0.82-0.73-1.77-1.1-2.85-1.1
c-1.09,0-2.05,0.36-2.86,1.09c-0.81,0.73-1.27,1.63-1.38,2.71l-0.06,0.54c0,0.12-0.07,0.18-0.2,0.18l-0.53,0.03
c-0.82,0.04-1.51,0.37-2.09,1s-0.86,1.37-0.86,2.22c0,0.87,0.3,1.62,0.9,2.26s1.33,0.98,2.18,1.02c0.11,0,0.17,0.06,0.17,0.18v1.33
c0,0.11-0.06,0.17-0.17,0.17c-1.34-0.06-2.47-0.57-3.4-1.53S4.64,18.24,4.64,16.91z M10.57,17.79c0-0.24,0.12-0.57,0.37-0.99
c0.24-0.42,0.47-0.75,0.68-1.01c0.21-0.24,0.34-0.38,0.38-0.42l0.36,0.4c0.26,0.28,0.5,0.61,0.72,1.02c0.22,0.4,0.33,0.74,0.33,1
c0,0.39-0.13,0.72-0.4,0.98c-0.27,0.26-0.6,0.39-1,0.39c-0.39,0-0.73-0.13-1.01-0.4C10.71,18.5,10.57,18.17,10.57,17.79z
M13.55,21.78c0-0.28,0.08-0.59,0.24-0.96s0.35-0.7,0.59-1.02c0.18-0.26,0.4-0.54,0.67-0.84c0.26-0.3,0.46-0.52,0.6-0.65
c0.07-0.06,0.15-0.14,0.24-0.23l0.24,0.23c0.38,0.33,0.8,0.82,1.27,1.46c0.24,0.33,0.43,0.68,0.59,1.04s0.23,0.68,0.23,0.97
c0,0.64-0.23,1.19-0.68,1.65s-1.01,0.68-1.66,0.68c-0.64,0-1.19-0.23-1.65-0.67C13.77,22.98,13.55,22.43,13.55,21.78z M15.02,15.12
c0-0.42,0.32-0.95,0.97-1.6l0.24,0.25c0.18,0.21,0.33,0.45,0.48,0.71c0.14,0.26,0.22,0.47,0.22,0.64c0,0.26-0.09,0.48-0.28,0.66
c-0.18,0.18-0.4,0.28-0.66,0.28c-0.27,0-0.5-0.09-0.69-0.28C15.11,15.6,15.02,15.38,15.02,15.12z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
<path d="M4.63,16.91c0,1.11,0.33,2.1,0.99,2.97s1.52,1.47,2.58,1.79l-0.66,1.68c-0.03,0.14,0.02,0.22,0.14,0.22h2.13l-0.98,4.3h0.28
l3.92-5.75c0.04-0.04,0.04-0.09,0.01-0.14c-0.03-0.05-0.08-0.07-0.15-0.07h-2.18l2.48-4.64c0.07-0.14,0.02-0.22-0.14-0.22h-2.94
c-0.09,0-0.17,0.05-0.23,0.15l-1.07,2.87c-0.71-0.18-1.3-0.57-1.77-1.16c-0.47-0.59-0.7-1.26-0.7-2.01c0-0.83,0.28-1.55,0.85-2.17
c0.57-0.61,1.27-0.97,2.1-1.07l0.53-0.07c0.13,0,0.2-0.06,0.2-0.18l0.07-0.51c0.11-1.08,0.56-1.99,1.37-2.72
c0.81-0.73,1.76-1.1,2.85-1.1c1.09,0,2.04,0.37,2.85,1.1c0.82,0.73,1.28,1.64,1.4,2.72l0.07,0.58c0,0.11,0.06,0.17,0.18,0.17h1.6
c0.91,0,1.68,0.32,2.32,0.95c0.64,0.63,0.97,1.4,0.97,2.28c0,0.85-0.3,1.59-0.89,2.21c-0.59,0.62-1.33,0.97-2.2,1.04
c-0.13,0-0.2,0.06-0.2,0.18v1.37c0,0.11,0.07,0.17,0.2,0.17c1.33-0.04,2.46-0.55,3.39-1.51s1.39-2.11,1.39-3.45
c0-0.9-0.22-1.73-0.67-2.49c-0.44-0.76-1.05-1.36-1.81-1.8c-0.77-0.44-1.6-0.66-2.5-0.66H20.1c-0.33-1.33-1.04-2.42-2.11-3.26
s-2.3-1.27-3.68-1.27c-1.41,0-2.67,0.44-3.76,1.31s-1.79,1.99-2.1,3.36c-1.11,0.26-2.02,0.83-2.74,1.73S4.63,15.76,4.63,16.91z
M12.77,26.62c0,0.39,0.19,0.65,0.58,0.77c0.01,0,0.05,0,0.11,0.01c0.06,0.01,0.11,0.01,0.14,0.01c0.17,0,0.33-0.05,0.49-0.15
c0.16-0.1,0.27-0.26,0.32-0.48l2.25-8.69c0.06-0.24,0.04-0.45-0.07-0.65c-0.11-0.19-0.27-0.32-0.5-0.39
c-0.17-0.02-0.26-0.03-0.26-0.03c-0.16,0-0.32,0.05-0.47,0.15c-0.15,0.1-0.26,0.25-0.31,0.45l-2.26,8.72
C12.78,26.44,12.77,26.53,12.77,26.62z M16.93,23.56c0,0.13,0.03,0.26,0.1,0.38c0.14,0.22,0.31,0.37,0.51,0.44
c0.11,0.03,0.21,0.05,0.3,0.05s0.2-0.02,0.32-0.08c0.21-0.09,0.35-0.28,0.42-0.57l1.44-5.67c0.03-0.14,0.05-0.23,0.05-0.27
c0-0.15-0.05-0.3-0.16-0.45s-0.26-0.26-0.46-0.32c-0.17-0.02-0.26-0.03-0.26-0.03c-0.17,0-0.33,0.05-0.47,0.15
c-0.14,0.1-0.24,0.25-0.3,0.45l-1.46,5.7c0,0.02,0,0.05-0.01,0.11C16.93,23.5,16.93,23.53,16.93,23.56z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

+57
View File
@@ -338,6 +338,63 @@ STR_LINK: "[link]"
STR_SCREENSHOT_BUTTON: "Take screenshot"
STR_AUTO_TURN_ENABLED: "Auto Turn Enabled: "
STR_AUTO_TURN_PAGES_PER_MIN: "Auto Turn (Pages Per Minute)"
STR_WEATHER: "Weather"
STR_WEATHER_LOCATION: "Location"
STR_WEATHER_NO_LOCATION: "No location set for weather"
STR_WEATHER_FETCH_FAILED: "Failed to fetch weather"
STR_WEATHER_SETTINGS: "Weather Settings"
STR_WEATHER_SETTINGS_SHORT: "Settings"
STR_WEATHER_REFRESH: "Refresh"
STR_WEATHER_FEELS_LIKE: "Feels like"
STR_WEATHER_HUMIDITY: "Humidity"
STR_WEATHER_WIND: "Wind"
STR_WEATHER_PRESSURE: "Pressure"
STR_WEATHER_LAST_UPDATED: "Last updated"
STR_WEATHER_PRECIP: "Precip."
STR_WEATHER_PRECIP_UNIT: "Precipitation Unit"
STR_WEATHER_WIND_UNIT: "Wind Speed Unit"
STR_WEATHER_TEMP_UNIT: "Temperature Unit"
STR_WEATHER_48H_FORECAST: "48-Hour Forecast"
STR_WEATHER_SEARCH_CITY: "Search City"
STR_WEATHER_LONGITUDE: "Longitude"
STR_WEATHER_LATITUDE: "Latitude"
STR_WEATHER_SEARCH_RESULTS: "Search Results"
STR_WEATHER_DAY_MON: "Mon"
STR_WEATHER_DAY_TUE: "Tue"
STR_WEATHER_DAY_WED: "Wed"
STR_WEATHER_DAY_THU: "Thu"
STR_WEATHER_DAY_FRI: "Fri"
STR_WEATHER_DAY_SAT: "Sat"
STR_WEATHER_DAY_SUN: "Sun"
STR_WEATHER_DESC_CLEAR_SKY: "Clear sky"
STR_WEATHER_DESC_MAINLY_CLEAR: "Mainly clear"
STR_WEATHER_DESC_PARTLY_CLOUDY: "Partly cloudy"
STR_WEATHER_DESC_OVERCAST: "Overcast"
STR_WEATHER_DESC_FOG: "Fog"
STR_WEATHER_DESC_RIME_FOG: "Rime fog"
STR_WEATHER_DESC_LIGHT_DRIZZLE: "Light drizzle"
STR_WEATHER_DESC_DRIZZLE: "Drizzle"
STR_WEATHER_DESC_DENSE_DRIZZLE: "Dense drizzle"
STR_WEATHER_DESC_FREEZING_DRIZZLE: "Freezing drizzle"
STR_WEATHER_DESC_DENSE_FREEZING_DRIZZLE: "Dense freezing drizzle"
STR_WEATHER_DESC_SLIGHT_RAIN: "Slight rain"
STR_WEATHER_DESC_MODERATE_RAIN: "Moderate rain"
STR_WEATHER_DESC_HEAVY_RAIN: "Heavy rain"
STR_WEATHER_DESC_FREEZING_RAIN: "Freezing rain"
STR_WEATHER_DESC_HEAVY_FREEZING_RAIN: "Heavy freezing rain"
STR_WEATHER_DESC_SLIGHT_SNOW: "Slight snow"
STR_WEATHER_DESC_MODERATE_SNOW: "Moderate snow"
STR_WEATHER_DESC_HEAVY_SNOW: "Heavy snow"
STR_WEATHER_DESC_SNOW_GRAINS: "Snow grains"
STR_WEATHER_DESC_SLIGHT_SHOWERS: "Slight showers"
STR_WEATHER_DESC_MODERATE_SHOWERS: "Moderate showers"
STR_WEATHER_DESC_VIOLENT_SHOWERS: "Violent showers"
STR_WEATHER_DESC_SNOW_SHOWERS: "Snow showers"
STR_WEATHER_DESC_HEAVY_SNOW_SHOWERS: "Heavy snow showers"
STR_WEATHER_DESC_THUNDERSTORM: "Thunderstorm"
STR_WEATHER_DESC_THUNDERSTORM_HAIL: "Thunderstorm, hail"
STR_WEATHER_DESC_THUNDERSTORM_HEAVY_HAIL: "Thunderstorm, heavy hail"
STR_WEATHER_DESC_UNKNOWN: "Unknown"
STR_INFO: "Info"
STR_AUTHOR: "Author"
STR_SERIES: "Series"
+60
View File
@@ -320,6 +320,66 @@ STR_UPTIME: "Laufzeit"
STR_CHARGING: "Lädt"
STR_GATHERING_DATA: "Daten werden gesammelt..."
STR_READING: "Lese..."
STR_WEATHER: "Wetter"
STR_WEATHER_LOCATION: "Ort"
STR_WEATHER_NO_LOCATION: "Kein Ort für Wetter festgelegt"
STR_WEATHER_FETCH_FAILED: "Wetterdaten konnten nicht geladen werden"
STR_WEATHER_SETTINGS: "Wettereinstellungen"
STR_WEATHER_SETTINGS_SHORT: "Einstell."
STR_WEATHER_REFRESH: "Aktual."
STR_WEATHER_FEELS_LIKE: "Gefühlt"
STR_WEATHER_HUMIDITY: "Luftfeuchte"
STR_WEATHER_WIND: "Wind"
STR_WEATHER_PRESSURE: "Luftdruck"
STR_WEATHER_LAST_UPDATED: "Zuletzt aktualisiert"
STR_WEATHER_PRECIP: "Niederschl."
STR_WEATHER_PRECIP_UNIT: "Niederschlagseinheit"
STR_WEATHER_WIND_UNIT: "Windgeschwindigkeitseinheit"
STR_WEATHER_TEMP_UNIT: "Temperatureinheit"
STR_WEATHER_48H_FORECAST: "48-Stunden-Vorhersage"
STR_WEATHER_SEARCH_CITY: "Stadt suchen"
STR_WEATHER_LONGITUDE: "Längengrad"
STR_WEATHER_LATITUDE: "Breitengrad"
STR_WEATHER_SEARCH_RESULTS: "Suchergebnisse"
STR_WEATHER_DAY_MON: "Mo"
STR_WEATHER_DAY_TUE: "Di"
STR_WEATHER_DAY_WED: "Mi"
STR_WEATHER_DAY_THU: "Do"
STR_WEATHER_DAY_FRI: "Fr"
STR_WEATHER_DAY_SAT: "Sa"
STR_WEATHER_DAY_SUN: "So"
STR_WEATHER_DESC_CLEAR_SKY: "Klarer Himmel"
STR_WEATHER_DESC_MAINLY_CLEAR: "Überwiegend klar"
STR_WEATHER_DESC_PARTLY_CLOUDY: "Teilweise bewölkt"
STR_WEATHER_DESC_OVERCAST: "Bedeckt"
STR_WEATHER_DESC_FOG: "Nebel"
STR_WEATHER_DESC_RIME_FOG: "Raureifnebel"
STR_WEATHER_DESC_LIGHT_DRIZZLE: "Leichter Nieselregen"
STR_WEATHER_DESC_DRIZZLE: "Nieselregen"
STR_WEATHER_DESC_DENSE_DRIZZLE: "Dichter Nieselregen"
STR_WEATHER_DESC_FREEZING_DRIZZLE: "Gefrierender Nieselregen"
STR_WEATHER_DESC_DENSE_FREEZING_DRIZZLE: "Dichter gefrierender Nieselregen"
STR_WEATHER_DESC_SLIGHT_RAIN: "Leichter Regen"
STR_WEATHER_DESC_MODERATE_RAIN: "Mäßiger Regen"
STR_WEATHER_DESC_HEAVY_RAIN: "Starker Regen"
STR_WEATHER_DESC_FREEZING_RAIN: "Gefrierender Regen"
STR_WEATHER_DESC_HEAVY_FREEZING_RAIN: "Starker gefrierender Regen"
STR_WEATHER_DESC_SLIGHT_SNOW: "Leichter Schneefall"
STR_WEATHER_DESC_MODERATE_SNOW: "Mäßiger Schneefall"
STR_WEATHER_DESC_HEAVY_SNOW: "Starker Schneefall"
STR_WEATHER_DESC_SNOW_GRAINS: "Schneegriesel"
STR_WEATHER_DESC_SLIGHT_SHOWERS: "Leichte Schauer"
STR_WEATHER_DESC_MODERATE_SHOWERS: "Mäßige Schauer"
STR_WEATHER_DESC_VIOLENT_SHOWERS: "Heftige Schauer"
STR_WEATHER_DESC_SNOW_SHOWERS: "Schneeschauer"
STR_WEATHER_DESC_HEAVY_SNOW_SHOWERS: "Starke Schneeschauer"
STR_WEATHER_DESC_THUNDERSTORM: "Gewitter"
STR_WEATHER_DESC_THUNDERSTORM_HAIL: "Gewitter mit Hagel"
STR_WEATHER_DESC_THUNDERSTORM_HEAVY_HAIL: "Gewitter mit starkem Hagel"
STR_WEATHER_DESC_UNKNOWN: "Unbekannt"
STR_CREATE_FALLBACK_FOR_INVALID_TOC: "Ersatz für ungültiges Inhaltsverzeichnis erstellen"
STR_USE_CLOCK: "Uhr anzeigen"
STR_CLOCK_SETTINGS: "Uhr-Einstellungen"
+350
View File
@@ -0,0 +1,350 @@
#include "WeatherClient.h"
#include <ArduinoJson.h>
#include <HalStorage.h>
#include <Logging.h>
#include <ctime>
#include "../../src/network/HttpDownloader.h"
namespace {
constexpr char WEATHER_CACHE_FILE[] = "/.crosspoint/weather_cache.json";
std::string buildForecastUrl(const WeatherSettingsStore& settings) {
// Open-Meteo supports plain HTTP. Using HTTP here avoids TLS handshake
// instability observed on some ESP32-C3 builds.
std::string url = "http://api.open-meteo.com/v1/forecast?";
url += "latitude=" + std::to_string(settings.getLatitude());
url += "&longitude=" + std::to_string(settings.getLongitude());
url +=
"&current=temperature_2m,relative_humidity_2m,apparent_temperature,"
"weather_code,wind_speed_10m,wind_direction_10m,is_day,precipitation,uv_index";
url += "&hourly=temperature_2m,precipitation,precipitation_probability,weather_code,is_day";
url +=
"&daily=temperature_2m_max,temperature_2m_min,weather_code,"
"precipitation_sum,sunrise,sunset,uv_index_max";
url += "&timezone=auto&timeformat=unixtime";
url += std::string("&temperature_unit=") + settings.getTempUnitParam();
url += std::string("&wind_speed_unit=") + settings.getWindUnitParam();
url += std::string("&precipitation_unit=") + settings.getPrecipUnitParam();
url += "&forecast_days=" + std::to_string(settings.getForecastDays());
// Request 48 hours of hourly data
url += "&forecast_hours=48";
return url;
}
} // namespace
WeatherData WeatherClient::getWeather(const WeatherSettingsStore& settings, bool forceRefresh) {
if (!settings.hasLocation()) {
WeatherData data;
data.errorMessage = "No location configured";
return data;
}
if (!forceRefresh) {
WeatherData cached;
if (loadCache(cached) && cached.valid) {
time_t now;
time(&now);
if (now - cached.fetchedAt < CACHE_TTL_SECONDS) {
LOG_DBG("WEA", "Using cached weather data (age: %ld s)", (long)(now - cached.fetchedAt));
return cached;
}
LOG_DBG("WEA", "Cache expired (age: %ld s)", (long)(now - cached.fetchedAt));
// Cache-first behavior: return stale cache and let the caller decide
// whether/when to perform a network refresh.
return cached;
}
LOG_DBG("WEA", "No cache available; caller should establish network and force refresh");
WeatherData data;
data.errorMessage = "No cache";
return data;
}
return fetchFromApi(settings);
}
WeatherData WeatherClient::fetchFromApi(const WeatherSettingsStore& settings) {
WeatherData data;
std::string url = buildForecastUrl(settings);
LOG_DBG("WEA", "fetchFromApi[1] start");
LOG_DBG("WEA", "fetchFromApi[2] url length=%zu", url.size());
LOG_DBG("WEA", "Fetching weather from API");
std::string response;
LOG_DBG("WEA", "fetchFromApi[3] HttpDownloader::fetchUrl before");
if (!HttpDownloader::fetchUrl(url, response)) {
data.errorMessage = "Network error";
LOG_ERR("WEA", "Failed to fetch weather data");
return data;
}
LOG_DBG("WEA", "fetchFromApi[4] HttpDownloader::fetchUrl after; bytes=%zu", response.size());
LOG_DBG("WEA", "fetchFromApi[5] parseWeatherJson before");
if (!parseWeatherJson(response, data)) {
LOG_ERR("WEA", "Failed to parse weather JSON");
return data;
}
LOG_DBG("WEA", "fetchFromApi[6] parseWeatherJson after");
data.valid = true;
time(&data.fetchedAt);
saveCache(data);
LOG_DBG("WEA", "fetchFromApi[7] Weather data fetched and cached");
return data;
}
bool WeatherClient::parseWeatherJson(const std::string& json, WeatherData& data) {
JsonDocument doc;
auto error = deserializeJson(doc, json);
if (error) {
data.errorMessage = "JSON parse error";
LOG_ERR("WEA", "JSON parse error: %s", error.c_str());
return false;
}
// Check for API error
if (doc["error"] | false) {
data.errorMessage = doc["reason"] | "API error";
LOG_ERR("WEA", "API error: %s", data.errorMessage.c_str());
return false;
}
data.timezone = doc["timezone"] | std::string("");
data.utcOffsetSeconds = doc["utc_offset_seconds"] | 0;
// Parse current weather
JsonObject current = doc["current"];
if (current) {
data.current.temperature = current["temperature_2m"] | 0.0f;
data.current.apparentTemperature = current["apparent_temperature"] | 0.0f;
data.current.humidity = current["relative_humidity_2m"] | 0;
data.current.weatherCode = current["weather_code"] | 0;
data.current.windSpeed = current["wind_speed_10m"] | 0.0f;
data.current.windDirection = current["wind_direction_10m"] | 0;
data.current.precipitation = current["precipitation"] | 0.0f;
data.current.uvIndex = current["uv_index"] | 0.0f;
data.current.isDay = (current["is_day"] | 1) != 0;
}
// Parse daily forecast
JsonObject daily = doc["daily"];
if (daily) {
JsonArray times = daily["time"];
JsonArray tempMax = daily["temperature_2m_max"];
JsonArray tempMin = daily["temperature_2m_min"];
JsonArray codes = daily["weather_code"];
JsonArray precip = daily["precipitation_sum"];
JsonArray sunrise = daily["sunrise"];
JsonArray sunset = daily["sunset"];
JsonArray uvMax = daily["uv_index_max"];
size_t count = times.size();
data.daily.reserve(count);
for (size_t i = 0; i < count; i++) {
DailyForecast day;
day.date = times[i] | (time_t)0;
day.tempMax = tempMax[i] | 0.0f;
day.tempMin = tempMin[i] | 0.0f;
day.weatherCode = codes[i] | 0;
day.precipSum = precip[i] | 0.0f;
day.sunrise = sunrise[i] | (time_t)0;
day.sunset = sunset[i] | (time_t)0;
day.uvIndexMax = uvMax[i] | 0.0f;
data.daily.push_back(day);
}
}
// Parse hourly forecast
JsonObject hourly = doc["hourly"];
if (hourly) {
JsonArray times = hourly["time"];
JsonArray temp = hourly["temperature_2m"];
JsonArray precip = hourly["precipitation"];
JsonArray precipProb = hourly["precipitation_probability"];
JsonArray codes = hourly["weather_code"];
JsonArray isDay = hourly["is_day"];
size_t count = times.size();
data.hourly.reserve(count);
for (size_t i = 0; i < count; i++) {
HourlyForecast hour;
hour.time = times[i] | (time_t)0;
hour.temperature = temp[i] | 0.0f;
hour.precipitation = precip[i] | 0.0f;
hour.precipitationProbability = precipProb[i] | 0;
hour.weatherCode = codes[i] | 0;
hour.isDay = (isDay[i] | 1) != 0;
data.hourly.push_back(hour);
}
}
return true;
}
bool WeatherClient::saveCache(const WeatherData& data) {
Storage.mkdir("/.crosspoint");
JsonDocument doc;
doc["fetchedAt"] = data.fetchedAt;
doc["timezone"] = data.timezone;
doc["utcOffsetSeconds"] = data.utcOffsetSeconds;
// Current
JsonObject cur = doc["current"].to<JsonObject>();
cur["temperature"] = data.current.temperature;
cur["apparentTemperature"] = data.current.apparentTemperature;
cur["humidity"] = data.current.humidity;
cur["weatherCode"] = data.current.weatherCode;
cur["windSpeed"] = data.current.windSpeed;
cur["windDirection"] = data.current.windDirection;
cur["precipitation"] = data.current.precipitation;
cur["uvIndex"] = data.current.uvIndex;
cur["isDay"] = data.current.isDay;
// Daily
JsonArray dailyArr = doc["daily"].to<JsonArray>();
for (const auto& day : data.daily) {
JsonObject d = dailyArr.add<JsonObject>();
d["date"] = day.date;
d["tempMax"] = day.tempMax;
d["tempMin"] = day.tempMin;
d["weatherCode"] = day.weatherCode;
d["precipSum"] = day.precipSum;
d["uvIndexMax"] = day.uvIndexMax;
d["sunrise"] = day.sunrise;
d["sunset"] = day.sunset;
}
// Hourly
JsonArray hourlyArr = doc["hourly"].to<JsonArray>();
for (const auto& hour : data.hourly) {
JsonObject h = hourlyArr.add<JsonObject>();
h["time"] = hour.time;
h["temperature"] = hour.temperature;
h["precipitation"] = hour.precipitation;
h["precipProb"] = hour.precipitationProbability;
h["weatherCode"] = hour.weatherCode;
h["isDay"] = hour.isDay;
}
String json;
serializeJson(doc, json);
return Storage.writeFile(WEATHER_CACHE_FILE, json);
}
bool WeatherClient::loadCache(WeatherData& data) {
if (!Storage.exists(WEATHER_CACHE_FILE)) {
return false;
}
String json = Storage.readFile(WEATHER_CACHE_FILE);
if (json.isEmpty()) {
return false;
}
JsonDocument doc;
auto error = deserializeJson(doc, json);
if (error) {
LOG_ERR("WEA", "Cache parse error: %s", error.c_str());
return false;
}
data.fetchedAt = doc["fetchedAt"] | (time_t)0;
data.timezone = doc["timezone"] | std::string("");
data.utcOffsetSeconds = doc["utcOffsetSeconds"] | 0;
// Current
JsonObject cur = doc["current"];
if (cur) {
data.current.temperature = cur["temperature"] | 0.0f;
data.current.apparentTemperature = cur["apparentTemperature"] | 0.0f;
data.current.humidity = cur["humidity"] | 0;
data.current.weatherCode = cur["weatherCode"] | 0;
data.current.windSpeed = cur["windSpeed"] | 0.0f;
data.current.windDirection = cur["windDirection"] | 0;
data.current.precipitation = cur["precipitation"] | 0.0f;
data.current.uvIndex = cur["uvIndex"] | 0.0f;
data.current.isDay = cur["isDay"] | true;
}
// Daily
JsonArray dailyArr = doc["daily"].as<JsonArray>();
for (JsonObject d : dailyArr) {
DailyForecast day;
day.date = d["date"] | (time_t)0;
day.tempMax = d["tempMax"] | 0.0f;
day.tempMin = d["tempMin"] | 0.0f;
day.weatherCode = d["weatherCode"] | 0;
day.precipSum = d["precipSum"] | 0.0f;
day.uvIndexMax = d["uvIndexMax"] | 0.0f;
day.sunrise = d["sunrise"] | (time_t)0;
day.sunset = d["sunset"] | (time_t)0;
data.daily.push_back(day);
}
// Hourly
JsonArray hourlyArr = doc["hourly"].as<JsonArray>();
for (JsonObject h : hourlyArr) {
HourlyForecast hour;
hour.time = h["time"] | (time_t)0;
hour.temperature = h["temperature"] | 0.0f;
hour.precipitation = h["precipitation"] | 0.0f;
hour.precipitationProbability = h["precipProb"] | 0;
hour.weatherCode = h["weatherCode"] | 0;
hour.isDay = h["isDay"] | true;
data.hourly.push_back(hour);
}
data.valid = true;
LOG_DBG("WEA", "Cache loaded, fetchedAt=%ld", (long)data.fetchedAt);
return true;
}
std::vector<GeocodingResult> WeatherClient::searchCity(const std::string& query) {
std::vector<GeocodingResult> results;
std::string url = "https://geocoding-api.open-meteo.com/v1/search?name=";
// Simple URL encoding for the query
for (char c : query) {
if (c == ' ') {
url += "%20";
} else if (isalnum(c) || c == '-' || c == '_' || c == '.') {
url += c;
} else {
char buf[4];
snprintf(buf, sizeof(buf), "%%%02X", (unsigned char)c);
url += buf;
}
}
url += "&count=5&language=en";
std::string response;
if (!HttpDownloader::fetchUrl(url, response)) {
LOG_ERR("WEA", "Geocoding request failed");
return results;
}
JsonDocument doc;
auto error = deserializeJson(doc, response);
if (error) {
LOG_ERR("WEA", "Geocoding parse error: %s", error.c_str());
return results;
}
JsonArray arr = doc["results"].as<JsonArray>();
for (JsonObject obj : arr) {
GeocodingResult r;
r.name = obj["name"] | std::string("");
r.country = obj["country"] | std::string("");
r.admin1 = obj["admin1"] | std::string("");
r.latitude = obj["latitude"] | 0.0f;
r.longitude = obj["longitude"] | 0.0f;
results.push_back(r);
}
LOG_DBG("WEA", "Geocoding found %zu results for '%s'", results.size(), query.c_str());
return results;
}
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include <string>
#include "WeatherData.h"
#include "WeatherSettingsStore.h"
class WeatherClient {
public:
/// Fetch weather data, using cache if valid (< 30 min old).
/// If forceRefresh is true, always fetches from the API.
static WeatherData getWeather(const WeatherSettingsStore& settings, bool forceRefresh = false);
/// Search for cities by name via Open-Meteo geocoding API.
/// Returns up to 5 results.
static std::vector<GeocodingResult> searchCity(const std::string& query);
private:
static WeatherData fetchFromApi(const WeatherSettingsStore& settings);
static bool parseWeatherJson(const std::string& json, WeatherData& data);
static bool saveCache(const WeatherData& data);
static bool loadCache(WeatherData& data);
static constexpr int CACHE_TTL_SECONDS = 30 * 60; // 30 minutes
};
+56
View File
@@ -0,0 +1,56 @@
#pragma once
#include <cstdint>
#include <ctime>
#include <string>
#include <vector>
struct CurrentWeather {
float temperature = 0;
float apparentTemperature = 0;
int humidity = 0;
int weatherCode = 0;
float windSpeed = 0;
int windDirection = 0;
float precipitation = 0;
float uvIndex = 0;
bool isDay = true;
};
struct DailyForecast {
time_t date = 0;
float tempMax = 0;
float tempMin = 0;
int weatherCode = 0;
float precipSum = 0;
float uvIndexMax = 0;
time_t sunrise = 0;
time_t sunset = 0;
};
struct HourlyForecast {
time_t time = 0;
float temperature = 0;
float precipitation = 0;
int precipitationProbability = 0;
int weatherCode = 0;
bool isDay = true;
};
struct WeatherData {
CurrentWeather current;
std::vector<DailyForecast> daily;
std::vector<HourlyForecast> hourly;
std::string timezone;
int utcOffsetSeconds = 0;
time_t fetchedAt = 0;
bool valid = false;
std::string errorMessage;
};
struct GeocodingResult {
std::string name;
std::string country;
std::string admin1; // State/region
float latitude = 0;
float longitude = 0;
};
+409
View File
@@ -0,0 +1,409 @@
#include "WeatherIcons.h"
#include "WeatherIcons48.h"
// Weather icon glyph source attribution:
// https://github.com/erikflowers/weather-icons
// 64x64 monochrome icon bitmaps in this file are adapted from that icon set.
// ============================================================================
// 64x64 Large Weather Icons (1-bit, MSB-first, row-major)
// Each row = 64 pixels = 8 bytes. Total = 64 * 8 = 512 bytes per icon.
// Pixel ON (1) = white/clear, Pixel OFF (0) = black/drawn.
// These are "inverted" bitmaps - 0 bits are drawn as black pixels.
// ============================================================================
// Clear Day - Sun with rays
// clang-format off
static const uint8_t ICON_CLEAR_DAY_48[] = {
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFE,0x7F,0xFF,0xFF, 0xFF,0xFF,0xFE,0x7F,0xFF,0xFF,
0xFF,0xFF,0xFE,0x7F,0xFF,0xFF, 0xFF,0xFF,0xFE,0x7F,0xFF,0xFF, 0xFF,0xFF,0xFE,0x7F,0xFF,0xFF,
0xFF,0x8F,0xFE,0x7F,0xF1,0xFF, 0xFF,0x1F,0xFE,0x7F,0xF8,0xFF, 0xFF,0x3F,0x80,0x01,0xFC,0xFF,
0xFF,0x7F,0x00,0x00,0xFE,0xFF, 0xFF,0xFC,0x00,0x00,0x3F,0xFF, 0xFF,0xF8,0x00,0x00,0x1F,0xFF,
0xFF,0xF0,0x00,0x00,0x0F,0xFF, 0xFF,0xE0,0x00,0x00,0x07,0xFF, 0xFF,0xE0,0x00,0x00,0x07,0xFF,
0xFF,0xC0,0x00,0x00,0x03,0xFF, 0xFF,0xC0,0x00,0x00,0x03,0xFF, 0xE0,0xC0,0x00,0x00,0x03,0x07,
0xE0,0x80,0x00,0x00,0x01,0x07, 0xF0,0x80,0x00,0x00,0x01,0x0F, 0xF8,0x80,0x00,0x00,0x01,0x1F,
0xFC,0x80,0x00,0x00,0x01,0x3F, 0xFC,0x80,0x00,0x00,0x01,0x3F, 0xF8,0x80,0x00,0x00,0x01,0x1F,
0xF0,0x80,0x00,0x00,0x01,0x0F, 0xE0,0x80,0x00,0x00,0x01,0x07, 0xE0,0xC0,0x00,0x00,0x03,0x07,
0xFF,0xC0,0x00,0x00,0x03,0xFF, 0xFF,0xC0,0x00,0x00,0x03,0xFF, 0xFF,0xE0,0x00,0x00,0x07,0xFF,
0xFF,0xE0,0x00,0x00,0x07,0xFF, 0xFF,0xF0,0x00,0x00,0x0F,0xFF, 0xFF,0xF8,0x00,0x00,0x1F,0xFF,
0xFF,0xFC,0x00,0x00,0x3F,0xFF, 0xFF,0x7F,0x00,0x00,0xFE,0xFF, 0xFF,0x3F,0x80,0x01,0xFC,0xFF,
0xFF,0x1F,0xFE,0x7F,0xF8,0xFF, 0xFF,0x8F,0xFE,0x7F,0xF1,0xFF, 0xFF,0xFF,0xFE,0x7F,0xFF,0xFF,
0xFF,0xFF,0xFE,0x7F,0xFF,0xFF, 0xFF,0xFF,0xFE,0x7F,0xFF,0xFF, 0xFF,0xFF,0xFE,0x7F,0xFF,0xFF,
0xFF,0xFF,0xFE,0x7F,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
};
// Clear Night - Moon crescent
static const uint8_t ICON_CLEAR_NIGHT_48[] = {
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFC,0x0F,0xFF,0xFF, 0xFF,0xFF,0xF0,0x03,0xFF,0xFF,
0xFF,0xFF,0xE0,0x01,0xFF,0xFF, 0xFF,0xFF,0xC0,0x00,0xFF,0xFF, 0xFF,0xFF,0x80,0x00,0x7F,0xFF,
0xFF,0xFF,0x00,0x00,0x3F,0xFF, 0xFF,0xFF,0x00,0x00,0x3F,0xFF, 0xFF,0xFE,0x00,0x00,0x1F,0xFF,
0xFF,0xFE,0x00,0x00,0x3F,0xFF, 0xFF,0xFC,0x00,0x00,0x7F,0xFF, 0xFF,0xFC,0x00,0x00,0xFF,0xFF,
0xFF,0xFC,0x00,0x01,0xFF,0xFF, 0xFF,0xF8,0x00,0x03,0xFF,0xFF, 0xFF,0xF8,0x00,0x07,0xFF,0xFF,
0xFF,0xF8,0x00,0x07,0xFF,0xFF, 0xFF,0xF8,0x00,0x0F,0xFF,0xFF, 0xFF,0xF8,0x00,0x0F,0xFF,0xFF,
0xFF,0xF8,0x00,0x0F,0xFF,0xFF, 0xFF,0xF8,0x00,0x0F,0xFF,0xFF, 0xFF,0xF8,0x00,0x0F,0xFF,0xFF,
0xFF,0xF8,0x00,0x0F,0xFF,0xFF, 0xFF,0xF8,0x00,0x07,0xFF,0xFF, 0xFF,0xF8,0x00,0x07,0xFF,0xFF,
0xFF,0xF8,0x00,0x03,0xFF,0xFF, 0xFF,0xFC,0x00,0x01,0xFF,0xFF, 0xFF,0xFC,0x00,0x00,0xFF,0xFF,
0xFF,0xFC,0x00,0x00,0x7F,0xFF, 0xFF,0xFE,0x00,0x00,0x3F,0xFF, 0xFF,0xFE,0x00,0x00,0x1F,0xFF,
0xFF,0xFF,0x00,0x00,0x1F,0xFF, 0xFF,0xFF,0x00,0x00,0x3F,0xFF, 0xFF,0xFF,0x80,0x00,0x7F,0xFF,
0xFF,0xFF,0xC0,0x00,0xFF,0xFF, 0xFF,0xFF,0xE0,0x01,0xFF,0xFF, 0xFF,0xFF,0xF0,0x03,0xFF,0xFF,
0xFF,0xFF,0xFC,0x0F,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
};
// Partly Cloudy Day - Sun behind cloud
static const uint8_t ICON_PARTLY_CLOUDY_DAY_48[] = {
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFE,0x7F,0xFF,0xFF,
0xFF,0xFF,0xFE,0x7F,0xFF,0xFF, 0xFF,0xFF,0xFE,0x7F,0xFF,0xFF, 0xFF,0x9F,0xFE,0x7F,0xF9,0xFF,
0xFF,0x3F,0xFE,0x7F,0xFC,0xFF, 0xFF,0x7F,0x80,0x01,0xFE,0xFF, 0xFF,0xFF,0x00,0x00,0xFF,0xFF,
0xFF,0xFC,0x00,0x00,0x3F,0xFF, 0xFF,0xF8,0x00,0x00,0x1F,0xFF, 0xFF,0xF0,0x00,0x00,0x0F,0xFF,
0xFF,0xF0,0x00,0x00,0x0F,0xFF, 0xE0,0xE0,0x00,0x00,0x07,0x07, 0xF0,0xE0,0x00,0x00,0x07,0x0F,
0xF8,0xE0,0x00,0x00,0x07,0x1F, 0xFF,0xE0,0x00,0x00,0x07,0xFF, 0xFF,0xF0,0x00,0x00,0x0F,0xFF,
0xFF,0xF0,0x00,0x00,0x0F,0xFF, 0xFF,0xF8,0x00,0x00,0x1F,0xFF, 0xFF,0xFC,0x00,0x00,0x3F,0xFF,
0xFF,0xFE,0x00,0x00,0x7F,0xFF, 0xFF,0xFC,0x00,0x00,0x3F,0xFF, 0xFF,0xF0,0x00,0x00,0x0F,0xFF,
0xFF,0xE0,0x00,0x00,0x07,0xFF, 0xFF,0xC0,0x00,0x00,0x03,0xFF, 0xFF,0x80,0x00,0x00,0x01,0xFF,
0xFF,0x80,0x00,0x00,0x01,0xFF, 0xFF,0x00,0x00,0x00,0x00,0xFF, 0xFE,0x00,0x00,0x00,0x00,0x7F,
0xFE,0x00,0x00,0x00,0x00,0x7F, 0xFC,0x00,0x00,0x00,0x00,0x3F, 0xFC,0x00,0x00,0x00,0x00,0x3F,
0xFC,0x00,0x00,0x00,0x00,0x3F, 0xFC,0x00,0x00,0x00,0x00,0x3F, 0xFC,0x00,0x00,0x00,0x00,0x3F,
0xFE,0x00,0x00,0x00,0x00,0x7F, 0xFE,0x00,0x00,0x00,0x00,0x7F, 0xFF,0x00,0x00,0x00,0x00,0xFF,
0xFF,0x80,0x00,0x00,0x01,0xFF, 0xFF,0xC0,0x00,0x00,0x03,0xFF, 0xFF,0xF0,0x00,0x00,0x0F,0xFF,
0xFF,0xFE,0x00,0x00,0x7F,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
};
// Partly Cloudy Night - Moon behind cloud (reuse partly cloudy day for simplicity)
static const uint8_t* ICON_PARTLY_CLOUDY_NIGHT_48 = ICON_PARTLY_CLOUDY_DAY_48;
// Overcast - Full cloud
static const uint8_t ICON_OVERCAST_48[] = {
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFC,0x3F,0xFF,0xFF,
0xFF,0xFF,0xF0,0x0F,0xFF,0xFF, 0xFF,0xFF,0xE0,0x07,0xFF,0xFF, 0xFF,0xFF,0xC0,0x03,0xFF,0xFF,
0xFF,0xFF,0x80,0x01,0xFF,0xFF, 0xFF,0xFF,0x00,0x00,0xFF,0xFF, 0xFF,0xFF,0x00,0x00,0xFF,0xFF,
0xFF,0xFE,0x00,0x00,0x7F,0xFF, 0xFF,0xFC,0x00,0x00,0x3F,0xFF, 0xFF,0xF8,0x00,0x00,0x1F,0xFF,
0xFF,0xF0,0x00,0x00,0x0F,0xFF, 0xFF,0xE0,0x00,0x00,0x07,0xFF, 0xFF,0xC0,0x00,0x00,0x03,0xFF,
0xFF,0x80,0x00,0x00,0x01,0xFF, 0xFF,0x00,0x00,0x00,0x00,0xFF, 0xFE,0x00,0x00,0x00,0x00,0x7F,
0xFE,0x00,0x00,0x00,0x00,0x7F, 0xFC,0x00,0x00,0x00,0x00,0x3F, 0xFC,0x00,0x00,0x00,0x00,0x3F,
0xFC,0x00,0x00,0x00,0x00,0x3F, 0xFC,0x00,0x00,0x00,0x00,0x3F, 0xFC,0x00,0x00,0x00,0x00,0x3F,
0xFC,0x00,0x00,0x00,0x00,0x3F, 0xFC,0x00,0x00,0x00,0x00,0x3F, 0xFC,0x00,0x00,0x00,0x00,0x3F,
0xFE,0x00,0x00,0x00,0x00,0x7F, 0xFE,0x00,0x00,0x00,0x00,0x7F, 0xFF,0x00,0x00,0x00,0x00,0xFF,
0xFF,0x80,0x00,0x00,0x01,0xFF, 0xFF,0xC0,0x00,0x00,0x03,0xFF, 0xFF,0xF0,0x00,0x00,0x0F,0xFF,
0xFF,0xFE,0x00,0x00,0x7F,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
};
// Rain - Cloud with rain drops
static const uint8_t ICON_RAIN_48[] = {
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFC,0x3F,0xFF,0xFF, 0xFF,0xFF,0xF0,0x0F,0xFF,0xFF,
0xFF,0xFF,0xE0,0x07,0xFF,0xFF, 0xFF,0xFF,0xC0,0x03,0xFF,0xFF, 0xFF,0xFF,0x80,0x01,0xFF,0xFF,
0xFF,0xFF,0x00,0x00,0xFF,0xFF, 0xFF,0xFE,0x00,0x00,0x7F,0xFF, 0xFF,0xF8,0x00,0x00,0x1F,0xFF,
0xFF,0xF0,0x00,0x00,0x0F,0xFF, 0xFF,0xE0,0x00,0x00,0x07,0xFF, 0xFF,0xC0,0x00,0x00,0x03,0xFF,
0xFF,0x80,0x00,0x00,0x01,0xFF, 0xFF,0x00,0x00,0x00,0x00,0xFF, 0xFE,0x00,0x00,0x00,0x00,0x7F,
0xFC,0x00,0x00,0x00,0x00,0x3F, 0xFC,0x00,0x00,0x00,0x00,0x3F, 0xFC,0x00,0x00,0x00,0x00,0x3F,
0xFC,0x00,0x00,0x00,0x00,0x3F, 0xFE,0x00,0x00,0x00,0x00,0x7F, 0xFF,0x00,0x00,0x00,0x00,0xFF,
0xFF,0x80,0x00,0x00,0x01,0xFF, 0xFF,0xE0,0x00,0x00,0x07,0xFF, 0xFF,0xFC,0x00,0x00,0x3F,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFB,0xDF,0xBF,0x7F,0xFF,
0xFF,0xF3,0xCF,0x9F,0x3F,0xFF, 0xFF,0xF3,0xCF,0x9F,0x3F,0xFF, 0xFF,0xE7,0xE7,0xCF,0x9F,0xFF,
0xFF,0xE7,0xE7,0xCF,0x9F,0xFF, 0xFF,0xCF,0xF3,0xE7,0xCF,0xFF, 0xFF,0xCF,0xF3,0xE7,0xCF,0xFF,
0xFF,0xCF,0xF3,0xE7,0xCF,0xFF, 0xFF,0xDF,0xFB,0xF7,0xEF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
};
// Snow - Cloud with snowflakes
static const uint8_t ICON_SNOW_48[] = {
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFC,0x3F,0xFF,0xFF, 0xFF,0xFF,0xF0,0x0F,0xFF,0xFF,
0xFF,0xFF,0xE0,0x07,0xFF,0xFF, 0xFF,0xFF,0xC0,0x03,0xFF,0xFF, 0xFF,0xFF,0x80,0x01,0xFF,0xFF,
0xFF,0xFF,0x00,0x00,0xFF,0xFF, 0xFF,0xFE,0x00,0x00,0x7F,0xFF, 0xFF,0xF8,0x00,0x00,0x1F,0xFF,
0xFF,0xF0,0x00,0x00,0x0F,0xFF, 0xFF,0xE0,0x00,0x00,0x07,0xFF, 0xFF,0xC0,0x00,0x00,0x03,0xFF,
0xFF,0x80,0x00,0x00,0x01,0xFF, 0xFF,0x00,0x00,0x00,0x00,0xFF, 0xFE,0x00,0x00,0x00,0x00,0x7F,
0xFC,0x00,0x00,0x00,0x00,0x3F, 0xFC,0x00,0x00,0x00,0x00,0x3F, 0xFC,0x00,0x00,0x00,0x00,0x3F,
0xFC,0x00,0x00,0x00,0x00,0x3F, 0xFE,0x00,0x00,0x00,0x00,0x7F, 0xFF,0x00,0x00,0x00,0x00,0xFF,
0xFF,0x80,0x00,0x00,0x01,0xFF, 0xFF,0xE0,0x00,0x00,0x07,0xFF, 0xFF,0xFC,0x00,0x00,0x3F,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xF9,0xE7,0xCF,0x3F,0xFF,
0xFF,0xFC,0xF3,0xE7,0x9F,0xFF, 0xFF,0xFE,0x79,0xF3,0xCF,0xFF, 0xFF,0xFF,0x3C,0xF9,0xE7,0xFF,
0xFF,0xFE,0x79,0xF3,0xCF,0xFF, 0xFF,0xFC,0xF3,0xE7,0x9F,0xFF, 0xFF,0xF9,0xE7,0xCF,0x3F,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
};
// Thunderstorm - Cloud with lightning bolt
static const uint8_t ICON_THUNDERSTORM_48[] = {
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFC,0x3F,0xFF,0xFF, 0xFF,0xFF,0xF0,0x0F,0xFF,0xFF,
0xFF,0xFF,0xE0,0x07,0xFF,0xFF, 0xFF,0xFF,0xC0,0x03,0xFF,0xFF, 0xFF,0xFF,0x80,0x01,0xFF,0xFF,
0xFF,0xFF,0x00,0x00,0xFF,0xFF, 0xFF,0xFE,0x00,0x00,0x7F,0xFF, 0xFF,0xF8,0x00,0x00,0x1F,0xFF,
0xFF,0xF0,0x00,0x00,0x0F,0xFF, 0xFF,0xE0,0x00,0x00,0x07,0xFF, 0xFF,0xC0,0x00,0x00,0x03,0xFF,
0xFF,0x80,0x00,0x00,0x01,0xFF, 0xFF,0x00,0x00,0x00,0x00,0xFF, 0xFE,0x00,0x00,0x00,0x00,0x7F,
0xFC,0x00,0x00,0x00,0x00,0x3F, 0xFC,0x00,0x00,0x00,0x00,0x3F, 0xFC,0x00,0x00,0x00,0x00,0x3F,
0xFC,0x00,0x00,0x00,0x00,0x3F, 0xFE,0x00,0x00,0x00,0x00,0x7F, 0xFF,0x00,0x00,0x00,0x00,0xFF,
0xFF,0x80,0x00,0x00,0x01,0xFF, 0xFF,0xE0,0x00,0x00,0x07,0xFF, 0xFF,0xFC,0x00,0x00,0x3F,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xF8,0xFF,0xFF,0xFF, 0xFF,0xFF,0xF1,0xFF,0xFF,0xFF,
0xFF,0xFF,0xE3,0xFF,0xFF,0xFF, 0xFF,0xFF,0xC7,0xFF,0xFF,0xFF, 0xFF,0xFF,0x80,0x3F,0xFF,0xFF,
0xFF,0xFF,0x00,0x7F,0xFF,0xFF, 0xFF,0xFF,0xF0,0xFF,0xFF,0xFF, 0xFF,0xFF,0xE1,0xFF,0xFF,0xFF,
0xFF,0xFF,0xC3,0xFF,0xFF,0xFF, 0xFF,0xFF,0x87,0xFF,0xFF,0xFF, 0xFF,0xFF,0x0F,0xFF,0xFF,0xFF,
0xFF,0xFF,0x1F,0xFF,0xFF,0xFF, 0xFF,0xFF,0x3F,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
};
// Fog - Horizontal lines
static const uint8_t ICON_FOG_48[] = {
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0x80,0x00,0x00,0x01,0xFF, 0xFF,0x00,0x00,0x00,0x00,0xFF,
0xFF,0x80,0x00,0x00,0x01,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xC0,0x00,0x00,0x03,0xFF, 0xFF,0x80,0x00,0x00,0x01,0xFF, 0xFF,0xC0,0x00,0x00,0x03,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0x80,0x00,0x00,0x01,0xFF,
0xFF,0x00,0x00,0x00,0x00,0xFF, 0xFF,0x80,0x00,0x00,0x01,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xC0,0x00,0x00,0x03,0xFF, 0xFF,0x80,0x00,0x00,0x01,0xFF,
0xFF,0xC0,0x00,0x00,0x03,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0x80,0x00,0x00,0x01,0xFF, 0xFF,0x00,0x00,0x00,0x00,0xFF, 0xFF,0x80,0x00,0x00,0x01,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
};
// clang-format on
// ============================================================================
// 24x24 Small Weather Icons (1-bit, MSB-first)
// Each row = 24 pixels = 3 bytes. Total = 24 * 3 = 72 bytes per icon.
// ============================================================================
#if WEATHER_ENABLE_SMALL_ICONS
// clang-format off
// Small Clear Day - Sun
static const uint8_t ICON_CLEAR_DAY_24[] = {
0xFF,0xFF,0xFF, 0xFF,0xE7,0xFF, 0xFF,0xE7,0xFF, 0xFF,0xE7,0xFF,
0xF3,0xE7,0xCF, 0xF9,0x00,0x9F, 0xFF,0x00,0xFF, 0xFE,0x00,0x7F,
0xFC,0x00,0x3F, 0xFC,0x00,0x3F, 0xE0,0x00,0x07, 0xE0,0x00,0x07,
0xE0,0x00,0x07, 0xE0,0x00,0x07, 0xFC,0x00,0x3F, 0xFC,0x00,0x3F,
0xFE,0x00,0x7F, 0xFF,0x00,0xFF, 0xF9,0x00,0x9F, 0xF3,0xE7,0xCF,
0xFF,0xE7,0xFF, 0xFF,0xE7,0xFF, 0xFF,0xE7,0xFF, 0xFF,0xFF,0xFF,
};
// Small Clear Night - Moon
static const uint8_t ICON_CLEAR_NIGHT_24[] = {
0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF, 0xFF,0xF0,0xFF, 0xFF,0xE0,0x7F,
0xFF,0xC0,0x3F, 0xFF,0x80,0x3F, 0xFF,0x80,0x7F, 0xFF,0x00,0xFF,
0xFF,0x01,0xFF, 0xFF,0x03,0xFF, 0xFF,0x03,0xFF, 0xFF,0x03,0xFF,
0xFF,0x03,0xFF, 0xFF,0x03,0xFF, 0xFF,0x01,0xFF, 0xFF,0x00,0xFF,
0xFF,0x80,0x7F, 0xFF,0x80,0x3F, 0xFF,0xC0,0x3F, 0xFF,0xE0,0x7F,
0xFF,0xF0,0xFF, 0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,
};
// Small Partly Cloudy
static const uint8_t ICON_PARTLY_CLOUDY_24[] = {
0xFF,0xFF,0xFF, 0xFF,0xE7,0xFF, 0xFB,0xE7,0xDF, 0xFD,0x00,0xBF,
0xFF,0x00,0xFF, 0xFE,0x00,0x7F, 0xFE,0x00,0x7F, 0xFC,0x00,0x3F,
0xFF,0x00,0x7F, 0xFE,0x00,0x3F, 0xFC,0x00,0x1F, 0xF8,0x00,0x0F,
0xF0,0x00,0x0F, 0xF0,0x00,0x07, 0xE0,0x00,0x07, 0xE0,0x00,0x07,
0xE0,0x00,0x07, 0xE0,0x00,0x07, 0xF0,0x00,0x0F, 0xF8,0x00,0x1F,
0xFF,0x00,0xFF, 0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,
};
// Small Overcast - Cloud
static const uint8_t ICON_OVERCAST_24[] = {
0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,
0xFF,0xF0,0xFF, 0xFF,0xE0,0x7F, 0xFF,0xC0,0x3F, 0xFF,0x80,0x1F,
0xFF,0x00,0x1F, 0xFE,0x00,0x0F, 0xFC,0x00,0x07, 0xF8,0x00,0x07,
0xF0,0x00,0x03, 0xE0,0x00,0x03, 0xE0,0x00,0x03, 0xE0,0x00,0x03,
0xE0,0x00,0x03, 0xF0,0x00,0x07, 0xF8,0x00,0x0F, 0xFE,0x00,0x7F,
0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,
};
// Small Rain
static const uint8_t ICON_RAIN_24[] = {
0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF, 0xFF,0xF0,0xFF, 0xFF,0xE0,0x7F,
0xFF,0x80,0x1F, 0xFF,0x00,0x0F, 0xFE,0x00,0x07, 0xFC,0x00,0x07,
0xF0,0x00,0x03, 0xE0,0x00,0x03, 0xE0,0x00,0x03, 0xE0,0x00,0x03,
0xF0,0x00,0x07, 0xF8,0x00,0x0F, 0xFF,0x00,0xFF, 0xFF,0xFF,0xFF,
0xFE,0xF7,0xBF, 0xFD,0xEF,0x7F, 0xFD,0xEF,0x7F, 0xFB,0xDF,0x7F,
0xFB,0xDF,0xFF, 0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,
};
// Small Snow
static const uint8_t ICON_SNOW_24[] = {
0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF, 0xFF,0xF0,0xFF, 0xFF,0xE0,0x7F,
0xFF,0x80,0x1F, 0xFF,0x00,0x0F, 0xFE,0x00,0x07, 0xFC,0x00,0x07,
0xF0,0x00,0x03, 0xE0,0x00,0x03, 0xE0,0x00,0x03, 0xE0,0x00,0x03,
0xF0,0x00,0x07, 0xF8,0x00,0x0F, 0xFF,0x00,0xFF, 0xFF,0xFF,0xFF,
0xFE,0xF7,0xBF, 0xFF,0x7B,0xDF, 0xFE,0xF7,0xBF, 0xFF,0x7B,0xDF,
0xFE,0xF7,0xBF, 0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,
};
// Small Thunderstorm
static const uint8_t ICON_THUNDERSTORM_24[] = {
0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF, 0xFF,0xF0,0xFF, 0xFF,0xE0,0x7F,
0xFF,0x80,0x1F, 0xFF,0x00,0x0F, 0xFE,0x00,0x07, 0xFC,0x00,0x07,
0xF0,0x00,0x03, 0xE0,0x00,0x03, 0xE0,0x00,0x03, 0xE0,0x00,0x03,
0xF0,0x00,0x07, 0xF8,0x00,0x0F, 0xFF,0x00,0xFF, 0xFF,0xFF,0xFF,
0xFF,0xF1,0xFF, 0xFF,0xE3,0xFF, 0xFF,0xC0,0xFF, 0xFF,0xF1,0xFF,
0xFF,0xE3,0xFF, 0xFF,0xC7,0xFF, 0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,
};
// Small Fog
static const uint8_t ICON_FOG_24[] = {
0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF, 0xF0,0x00,0x0F, 0xE0,0x00,0x07,
0xF0,0x00,0x0F, 0xFF,0xFF,0xFF, 0xF8,0x00,0x1F, 0xF0,0x00,0x0F,
0xF8,0x00,0x1F, 0xFF,0xFF,0xFF, 0xF0,0x00,0x0F, 0xE0,0x00,0x07,
0xF0,0x00,0x0F, 0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,
};
// clang-format on
#endif // WEATHER_ENABLE_SMALL_ICONS
// ============================================================================
// Lookup functions
// ============================================================================
const uint8_t* getWeatherIconLarge(WeatherIconType type) {
switch (type) {
case WeatherIconType::CLEAR_DAY:
return WI48_CLEAR_DAY;
case WeatherIconType::CLEAR_NIGHT:
return WI48_CLEAR_NIGHT;
case WeatherIconType::PARTLY_CLOUDY_DAY:
return WI48_PARTLY_CLOUDY_DAY;
case WeatherIconType::PARTLY_CLOUDY_NIGHT:
return WI48_PARTLY_CLOUDY_NIGHT;
case WeatherIconType::OVERCAST:
return WI48_OVERCAST;
case WeatherIconType::FOG:
return WI48_FOG;
case WeatherIconType::DRIZZLE:
return WI48_DRIZZLE;
case WeatherIconType::RAIN:
case WeatherIconType::SHOWERS:
return WI48_RAIN;
case WeatherIconType::SNOW:
return WI48_SNOW;
case WeatherIconType::THUNDERSTORM:
return WI48_THUNDERSTORM;
default:
return WI48_OVERCAST;
}
}
#if WEATHER_ENABLE_SMALL_ICONS
const uint8_t* getWeatherIconSmall(WeatherIconType type) {
switch (type) {
case WeatherIconType::CLEAR_DAY:
return ICON_CLEAR_DAY_24;
case WeatherIconType::CLEAR_NIGHT:
return ICON_CLEAR_NIGHT_24;
case WeatherIconType::PARTLY_CLOUDY_DAY:
case WeatherIconType::PARTLY_CLOUDY_NIGHT:
return ICON_PARTLY_CLOUDY_24;
case WeatherIconType::OVERCAST:
return ICON_OVERCAST_24;
case WeatherIconType::FOG:
return ICON_FOG_24;
case WeatherIconType::DRIZZLE:
case WeatherIconType::RAIN:
case WeatherIconType::SHOWERS:
return ICON_RAIN_24;
case WeatherIconType::SNOW:
return ICON_SNOW_24;
case WeatherIconType::THUNDERSTORM:
return ICON_THUNDERSTORM_24;
default:
return ICON_OVERCAST_24;
}
}
#endif // WEATHER_ENABLE_SMALL_ICONS
const char* getWeatherDescription(int wmoCode) {
switch (wmoCode) {
case 0:
return "Clear sky";
case 1:
return "Mainly clear";
case 2:
return "Partly cloudy";
case 3:
return "Overcast";
case 45:
return "Fog";
case 48:
return "Rime fog";
case 51:
return "Light drizzle";
case 53:
return "Drizzle";
case 55:
return "Dense drizzle";
case 56:
return "Freezing drizzle";
case 57:
return "Dense freezing drizzle";
case 61:
return "Slight rain";
case 63:
return "Moderate rain";
case 65:
return "Heavy rain";
case 66:
return "Freezing rain";
case 67:
return "Heavy freezing rain";
case 71:
return "Slight snow";
case 73:
return "Moderate snow";
case 75:
return "Heavy snow";
case 77:
return "Snow grains";
case 80:
return "Slight showers";
case 81:
return "Moderate showers";
case 82:
return "Violent showers";
case 85:
return "Snow showers";
case 86:
return "Heavy snow showers";
case 95:
return "Thunderstorm";
case 96:
return "Thunderstorm, hail";
case 99:
return "Thunderstorm, heavy hail";
default:
return "Unknown";
}
}
const char* getWindDirectionText(int degrees) {
// Normalize to 0-360
degrees = ((degrees % 360) + 360) % 360;
if (degrees >= 338 || degrees < 23) return "N";
if (degrees < 68) return "NE";
if (degrees < 113) return "E";
if (degrees < 158) return "SE";
if (degrees < 203) return "S";
if (degrees < 248) return "SW";
if (degrees < 293) return "W";
return "NW";
}
+87
View File
@@ -0,0 +1,87 @@
#pragma once
#include <cstdint>
// Set to 1 (for example via build flag -DWEATHER_ENABLE_SMALL_ICONS=1)
// to include 24x24 weather icon assets and API.
#ifndef WEATHER_ENABLE_SMALL_ICONS
#define WEATHER_ENABLE_SMALL_ICONS 0
#endif
// Weather icon size constants
constexpr int WEATHER_ICON_LARGE = 64; // Large icon for current weather
constexpr int WEATHER_ICON_SMALL = 24; // Small icon for daily forecast
// WMO weather code to icon category mapping
enum class WeatherIconType {
CLEAR_DAY,
CLEAR_NIGHT,
PARTLY_CLOUDY_DAY,
PARTLY_CLOUDY_NIGHT,
OVERCAST,
FOG,
DRIZZLE,
RAIN,
SNOW,
SHOWERS,
THUNDERSTORM,
UNKNOWN
};
// Map WMO weather code + day/night to icon type
inline WeatherIconType getWeatherIconType(int wmoCode, bool isDay) {
switch (wmoCode) {
case 0:
return isDay ? WeatherIconType::CLEAR_DAY : WeatherIconType::CLEAR_NIGHT;
case 1:
case 2:
return isDay ? WeatherIconType::PARTLY_CLOUDY_DAY : WeatherIconType::PARTLY_CLOUDY_NIGHT;
case 3:
return WeatherIconType::OVERCAST;
case 45:
case 48:
return WeatherIconType::FOG;
case 51:
case 53:
case 55:
case 56:
case 57:
return WeatherIconType::DRIZZLE;
case 61:
case 63:
case 65:
case 66:
case 67:
return WeatherIconType::RAIN;
case 71:
case 73:
case 75:
case 77:
return WeatherIconType::SNOW;
case 80:
case 81:
case 82:
case 85:
case 86:
return WeatherIconType::SHOWERS;
case 95:
case 96:
case 99:
return WeatherIconType::THUNDERSTORM;
default:
return WeatherIconType::UNKNOWN;
}
}
// Get human-readable description for WMO weather code
const char* getWeatherDescription(int wmoCode);
// Get the appropriate large icon bitmap (64x64, 1-bit, MSB first)
const uint8_t* getWeatherIconLarge(WeatherIconType type);
// Get the appropriate small icon bitmap (24x24, 1-bit, MSB first)
#if WEATHER_ENABLE_SMALL_ICONS
const uint8_t* getWeatherIconSmall(WeatherIconType type);
#endif
// Wind direction arrow text (N, NE, E, SE, S, SW, W, NW)
const char* getWindDirectionText(int degrees);
+307
View File
@@ -0,0 +1,307 @@
#pragma once
#include <cstdint>
// Generated from erikflowers/weather-icons SVGs.
// 48x48, 1-bit, MSB-first, row-major.
// Regenerate with: python scripts/generate_weather_icons.py --fetch
// clang-format off
static const uint8_t WI48_CLEAR_DAY[] = {
0xFF, 0xFF, 0xFF, 0xFE, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xF8, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x1F, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xF8, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0x87, 0xFF, 0xFC,
0x3F, 0xFF, 0xE1, 0xFF, 0xFF, 0x83, 0xFF, 0xFC, 0x3F, 0xFF, 0xC1, 0xFF, 0xFF, 0x81, 0xFF, 0xFF, 0xFF, 0xFF, 0x81,
0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0xFF, 0xFF, 0xC0, 0x7F, 0xFF, 0xFF, 0xFE, 0x03, 0xFF, 0xFF, 0xE0,
0x3F, 0xFF, 0xFF, 0xFC, 0x07, 0xFF, 0xFF, 0xF0, 0x3F, 0xFF, 0xFF, 0xFC, 0x0F, 0xFF, 0xFF, 0xF8, 0x3F, 0xF8, 0x3F,
0xFC, 0x1F, 0xFF, 0xFF, 0xFC, 0x7F, 0xC0, 0x03, 0xFE, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFE, 0x00, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x00, 0x00, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0,
0x01, 0x80, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x1F, 0xF8, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x3F, 0xFC, 0x07,
0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x7F, 0xFE, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0x03, 0xFF, 0xFF, 0xFF,
0xFF, 0x81, 0xFF, 0xFF, 0x81, 0xFF, 0xFF, 0xFF, 0xFF, 0x83, 0xFF, 0xFF, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0x83, 0xFF,
0xFF, 0xC1, 0xFF, 0xFF, 0xC0, 0x3F, 0x83, 0xFF, 0xFF, 0xC1, 0xFC, 0x03, 0x80, 0x1F, 0x03, 0xFF, 0xFF, 0xE1, 0xF8,
0x01, 0x00, 0x1F, 0x07, 0xFF, 0xFF, 0xE0, 0xF8, 0x00, 0x00, 0x1F, 0x07, 0xFF, 0xFF, 0xE0, 0xF8, 0x00, 0x80, 0x1F,
0x03, 0xFF, 0xFF, 0xE1, 0xF8, 0x01, 0xFF, 0xFF, 0x83, 0xFF, 0xFF, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0x83, 0xFF, 0xFF,
0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0x83, 0xFF, 0xFF, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0x81, 0xFF, 0xFF, 0x81, 0xFF, 0xFF,
0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x7F, 0xFE, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0,
0x3F, 0xFC, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x1F, 0xF8, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x01, 0x80, 0x1F,
0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x00, 0x7F, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x7F, 0xC0, 0x03, 0xFE, 0x3F, 0xFF, 0xFF, 0xF8, 0x3F, 0xFE,
0x7F, 0xFC, 0x1F, 0xFF, 0xFF, 0xF0, 0x3F, 0xFF, 0xFF, 0xFC, 0x0F, 0xFF, 0xFF, 0xE0, 0x3F, 0xFF, 0xFF, 0xFC, 0x07,
0xFF, 0xFF, 0xC0, 0x7F, 0xFF, 0xFF, 0xFE, 0x03, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0xFF, 0xFF, 0x81,
0xFF, 0xFF, 0xFF, 0xFF, 0x81, 0xFF, 0xFF, 0x83, 0xFF, 0xFC, 0x3F, 0xFF, 0xC1, 0xFF, 0xFF, 0xC7, 0xFF, 0xFC, 0x3F,
0xFF, 0xE3, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x1F, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xF8, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xF8, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x1F, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x7F, 0xFF, 0xFF, 0xFF,
};
static const uint8_t WI48_CLEAR_NIGHT[] = {
0xFF, 0xFF, 0xFF, 0xC0, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x00, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0,
0x00, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x03, 0xFF,
0xFF, 0xFF, 0xFF, 0xFC, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0xFF, 0xFF,
0xE0, 0x00, 0x06, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x7E, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x01, 0xFE,
0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x07, 0xFE, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x1F, 0xFF, 0x01, 0xFF, 0xFF,
0xFF, 0xFE, 0x00, 0x3F, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFC, 0x00, 0x7F, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xF8, 0x00,
0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0xFF, 0xF8, 0x01, 0xFF, 0xFF, 0x80, 0x7F, 0xFF, 0xFF, 0xF0, 0x03, 0xFF, 0xFF, 0x80,
0x3F, 0xFF, 0xFF, 0xF0, 0x07, 0xFF, 0xFF, 0xC0, 0x3F, 0xFF, 0xFF, 0xE0, 0x0F, 0xFF, 0xFF, 0xC0, 0x1F, 0xFF, 0xFF,
0xE0, 0x0F, 0xFF, 0xFF, 0xE0, 0x0F, 0xFF, 0xFF, 0xC0, 0x1F, 0xFF, 0xFF, 0xF0, 0x07, 0xFF, 0xFF, 0xC0, 0x1F, 0xFF,
0xFF, 0xF0, 0x03, 0xFF, 0xFF, 0xC0, 0x3F, 0xFF, 0xFF, 0xF8, 0x00, 0xFF, 0xFF, 0x80, 0x3F, 0xFF, 0xFF, 0xFC, 0x00,
0x7F, 0xFF, 0x80, 0x3F, 0xFF, 0xFF, 0xFE, 0x00, 0x1F, 0xFF, 0x80, 0x7F, 0xFF, 0xFF, 0xFF, 0x00, 0x03, 0xFF, 0x80,
0x7F, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x03, 0x80, 0x7F, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x01, 0x00, 0x7F, 0xFF, 0xFF,
0xFF, 0xF0, 0x00, 0x01, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xF8, 0x00, 0x01, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x00,
0x01, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x01, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x01, 0x00, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x7F, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0x01, 0x80, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x01, 0x80, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x01,
0x80, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x03, 0x80, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x03, 0x80, 0x3F, 0xFF,
0xFF, 0xFF, 0xFF, 0xFC, 0x03, 0xC0, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x03, 0xC0, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF,
0xF8, 0x07, 0xC0, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x07, 0xE0, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x07, 0xE0,
0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x0F, 0xF0, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x0F, 0xF0, 0x03, 0xFF, 0xFF,
0xFF, 0xFF, 0x80, 0x1F, 0xF8, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x3F, 0xF8, 0x00, 0xFF, 0xFF, 0xFF, 0xFE, 0x00,
0x3F, 0xFC, 0x00, 0x7F, 0xFF, 0xFF, 0xFC, 0x00, 0x7F, 0xFE, 0x00, 0x1F, 0xFF, 0xFF, 0xF8, 0x00, 0xFF, 0xFF, 0x00,
0x0F, 0xFF, 0xFF, 0xE0, 0x00, 0xFF, 0xFF, 0x80, 0x03, 0xFF, 0xFF, 0x80, 0x01, 0xFF, 0xFF, 0xC0, 0x00, 0x7F, 0xFE,
0x00, 0x03, 0xFF, 0xFF, 0xE0, 0x00, 0x07, 0xC0, 0x00, 0x07, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF,
0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0x00,
0x00, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x00, 0x00, 0x1F,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x3F, 0xFF, 0xFF, 0xFF,
};
static const uint8_t WI48_PARTLY_CLOUDY_DAY[] = {
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xF8, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x7F, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF,
0xF9, 0xFF, 0xF8, 0x7F, 0xFC, 0xFF, 0xFF, 0xFF, 0xF0, 0xFF, 0xF8, 0xFF, 0xF8, 0x7F, 0xFF, 0xFF, 0xF0, 0x7F, 0xFF,
0xFF, 0xF0, 0x7F, 0xFF, 0xFF, 0xF8, 0x3F, 0xFF, 0xFF, 0xE0, 0xFF, 0xFF, 0xFF, 0xFC, 0x7F, 0xFF, 0xFF, 0xC0, 0xFF,
0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x3F, 0xC3, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0x80, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x0C, 0x00, 0x01,
0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x0F, 0x80, 0xFF, 0xFF, 0xFF, 0xFC, 0x00, 0x00, 0x3F, 0xE0, 0x7F, 0xFF, 0xFF,
0xF8, 0x00, 0x00, 0x7F, 0xF0, 0x7F, 0xFF, 0xFF, 0xF0, 0x1F, 0xE0, 0x3F, 0xF8, 0x3F, 0xFF, 0xFF, 0xE0, 0x7F, 0xF8,
0x3F, 0xFC, 0x3F, 0xFF, 0xFF, 0xE0, 0xFF, 0xFC, 0x1F, 0xFC, 0x1F, 0xFF, 0xFF, 0xC1, 0xFF, 0xFE, 0x0F, 0xFE, 0x1F,
0xFF, 0xFF, 0xC3, 0xFF, 0xFF, 0x0F, 0xFE, 0x1E, 0x00, 0xFF, 0x83, 0xFF, 0xFF, 0x0F, 0xFE, 0x1E, 0x00, 0xFF, 0x87,
0xFF, 0xFF, 0x87, 0xFE, 0x1E, 0x00, 0xFE, 0x07, 0xFF, 0xFF, 0x80, 0x3E, 0x1F, 0xEF, 0xF8, 0x07, 0xFF, 0xFF, 0x80,
0x0C, 0x1F, 0xFF, 0xF0, 0x07, 0xFF, 0xFF, 0x80, 0x04, 0x3F, 0xFF, 0xE0, 0x1F, 0xFF, 0xFF, 0xDC, 0x00, 0x3F, 0xFF,
0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x3F, 0xFF, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x7F, 0xFF, 0x83, 0xFF, 0xFF,
0xFF, 0xFF, 0xF0, 0xFF, 0xFF, 0x87, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x7F, 0xFF, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8,
0x7F, 0xFF, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x7F, 0xFF, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x7F, 0xFF, 0x0F,
0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x7F, 0xFF, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x71, 0xFF, 0x07, 0xFF, 0xFF, 0xFF,
0xFF, 0xF8, 0x70, 0xFF, 0x87, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x70, 0x7F, 0x83, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0xF8,
0x7F, 0x83, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0xFC, 0xFF, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xC1, 0xFF, 0xFF, 0xE0, 0x7F,
0xFF, 0xFF, 0xFF, 0x01, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00,
0x07, 0xFF, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
};
static const uint8_t WI48_PARTLY_CLOUDY_NIGHT[] = {
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x00, 0x03,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x01, 0x83, 0xFF, 0xFF, 0xFF, 0xFF, 0xC1, 0xF0, 0x0F, 0x83, 0xFF, 0xFF, 0xFF,
0xFC, 0x00, 0x00, 0x3F, 0x83, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x7F, 0x83, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x00,
0xFF, 0xC1, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0xFF, 0xC1, 0xFF, 0xFF, 0xFF, 0x00, 0x1C, 0x00, 0x7F, 0xC0, 0xFF,
0xFF, 0xFE, 0x01, 0xFF, 0x80, 0x3F, 0xE0, 0xFF, 0xFF, 0xFC, 0x07, 0xFF, 0xE0, 0x1F, 0xE0, 0x7F, 0xFF, 0xFC, 0x0F,
0xFF, 0xF8, 0x1F, 0xF0, 0x3F, 0xFF, 0xF8, 0x1F, 0xFF, 0xFC, 0x0F, 0xF0, 0x0F, 0xFF, 0xF8, 0x3F, 0xFF, 0xFC, 0x0F,
0xF8, 0x01, 0xFF, 0xF0, 0x3F, 0xFF, 0xFE, 0x07, 0xFC, 0x00, 0xFF, 0xF0, 0x7F, 0xFF, 0xFF, 0x07, 0xFE, 0x00, 0xFF,
0xE0, 0x7F, 0xFF, 0xFF, 0x03, 0xFF, 0x81, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0x00, 0x1F, 0xC1, 0xFF, 0x00, 0xFF, 0xFF,
0xFF, 0x80, 0x07, 0x81, 0xFC, 0x00, 0xFF, 0xFF, 0xFF, 0x80, 0x01, 0x03, 0xF8, 0x00, 0xFF, 0xFF, 0xFF, 0x80, 0x00,
0x03, 0xF0, 0x00, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x07, 0xE0, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x0F, 0xC0, 0x7F,
0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x1F, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x1F, 0x81, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFC, 0x0F, 0x83, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x0F, 0x83, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x0F,
0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x07, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x07, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0x07, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x0F, 0x83,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x0F, 0x81, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x0F, 0xC1, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFC, 0x1F, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x1F, 0xE0, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0,
0x3F, 0xE0, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x3F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xF8, 0x00,
0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00,
0x00, 0x0F, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
};
static const uint8_t WI48_OVERCAST[] = {
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x1F, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFE, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x03, 0xC0, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x1F,
0xF0, 0x3F, 0xFF, 0xFF, 0xFF, 0xF8, 0x3F, 0x3F, 0xFC, 0x1F, 0xFF, 0xFF, 0xFF, 0xC0, 0x03, 0xFF, 0xFE, 0x0F, 0xFF,
0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFE, 0x0F, 0xFF, 0xFF, 0xFE, 0x00, 0x00, 0x7F, 0xFF, 0x0F, 0xFF, 0xFF, 0xFC, 0x01,
0x80, 0x3F, 0xFF, 0x00, 0xFF, 0xFF, 0xF8, 0x1F, 0xF8, 0x1F, 0xFF, 0x00, 0x1F, 0xFF, 0xF0, 0x3F, 0xFC, 0x0F, 0xFF,
0x80, 0x0F, 0xFF, 0xE0, 0xFF, 0xFE, 0x07, 0xFF, 0x80, 0x07, 0xFF, 0xE0, 0xFF, 0xFF, 0x07, 0xFF, 0xFE, 0x03, 0xFF,
0xC1, 0xFF, 0xFF, 0x83, 0xFF, 0xFF, 0x83, 0xFF, 0xC3, 0xFF, 0xFF, 0x83, 0xFF, 0xFF, 0xC1, 0xFF, 0xC3, 0xFF, 0xFF,
0xC0, 0xFF, 0xFF, 0xE1, 0xFE, 0x03, 0xFF, 0xFF, 0xC0, 0x0F, 0xFF, 0xE0, 0xF8, 0x07, 0xFF, 0xFF, 0xC0, 0x03, 0xFF,
0xF0, 0xF0, 0x07, 0xFF, 0xFF, 0xC0, 0x01, 0xFF, 0xF0, 0xE0, 0x0F, 0xFF, 0xFF, 0xE0, 0x00, 0xFF, 0xF0, 0xC0, 0x7F,
0xFF, 0xFF, 0xFF, 0xE0, 0x7F, 0xF0, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x3F, 0xE0, 0x83, 0xFF, 0xFF, 0xFF, 0xFF,
0xF8, 0x3F, 0xE1, 0x87, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x1F, 0xC1, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x1F, 0x83,
0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x1C, 0x03, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x18, 0x07, 0x0F, 0xFF, 0xFF,
0xFF, 0xFF, 0xFE, 0x18, 0x0F, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x18, 0x3F, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE,
0x18, 0xFF, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x1F, 0xFF, 0x87, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x1F, 0xFF, 0x83,
0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x3F, 0xFF, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x3F, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF,
0xFF, 0xE0, 0x7F, 0xFF, 0xE0, 0x1F, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF,
0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0xFF, 0xFF, 0xC0,
0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
};
static const uint8_t WI48_FOG[] = {
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0,
0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x00, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFC, 0x00, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF,
0xF0, 0x1F, 0xF0, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x7F, 0xFC, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFE,
0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xC1, 0xFF, 0xFF, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0x83, 0xFF, 0xFF, 0x83, 0xFF, 0xFF,
0xFF, 0xFF, 0x83, 0xFF, 0xFF, 0x83, 0xFF, 0xFF, 0xFF, 0xFF, 0x87, 0xFF, 0xFF, 0xC3, 0xFF, 0xFF, 0xFF, 0xFE, 0x07,
0xFF, 0xFF, 0xC0, 0x0F, 0xFF, 0xFF, 0xF8, 0x07, 0xFF, 0xFF, 0xC0, 0x03, 0xFF, 0xFF, 0xE0, 0x07, 0xFF, 0xFF, 0xC0,
0x01, 0xFF, 0xFF, 0xC0, 0x0F, 0xFF, 0xFF, 0xC0, 0x00, 0xFF, 0xFF, 0x80, 0x7F, 0xFF, 0xFF, 0xFF, 0xC0, 0x7F, 0xFF,
0x81, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x3F, 0xFF, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x3F, 0xFF, 0x07, 0xFF, 0xFF,
0xFF, 0xFF, 0xFC, 0x1F, 0xFE, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFE, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x1F, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
0x0F, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0x80,
0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xE0,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
};
static const uint8_t WI48_DRIZZLE[] = {
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFE, 0x00, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x00,
0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x1F, 0x00, 0x1F, 0xFF,
0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xE0, 0x0F, 0xFF, 0xFF, 0xFF, 0xFE, 0x01, 0xFF, 0xF8, 0x07, 0xFF, 0xFF, 0xFF, 0xFC,
0x07, 0xFF, 0xFC, 0x07, 0xFF, 0xFF, 0xFF, 0xFC, 0x0F, 0xFF, 0xFE, 0x03, 0xFF, 0xFF, 0xFF, 0xF8, 0x1F, 0xFF, 0xFF,
0x03, 0xFF, 0xFF, 0xFF, 0xF8, 0x1F, 0xFF, 0xFF, 0x81, 0xFF, 0xFF, 0xFF, 0xF0, 0x3F, 0xFF, 0xFF, 0xC1, 0xFF, 0xFF,
0xFF, 0xF0, 0x3F, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xE0, 0x7F, 0xFF, 0xFF, 0xC0, 0x0F, 0xFF, 0xFF, 0x80, 0x7F,
0xFF, 0xFF, 0xE0, 0x01, 0xFF, 0xFE, 0x00, 0x7F, 0xFF, 0xFF, 0xE0, 0x00, 0x7F, 0xFC, 0x00, 0x7F, 0xFF, 0xFF, 0xE0,
0x00, 0x3F, 0xF8, 0x00, 0x7F, 0xFF, 0xFF, 0xE0, 0x00, 0x1F, 0xF0, 0x0F, 0xFF, 0xFF, 0xCF, 0xFF, 0xE0, 0x0F, 0xE0,
0x3F, 0xFF, 0xFF, 0x87, 0xFF, 0xFC, 0x07, 0xC0, 0x7F, 0xFF, 0xFF, 0x87, 0xFF, 0xFE, 0x03, 0xC0, 0xFF, 0xFF, 0xFF,
0x03, 0xFF, 0xFF, 0x03, 0x81, 0xFF, 0xFF, 0xFF, 0x03, 0xFF, 0xFF, 0x81, 0x83, 0xFF, 0xFF, 0xFF, 0x03, 0xFF, 0xFF,
0xC1, 0x83, 0xFF, 0xFC, 0xFF, 0x87, 0xFF, 0xFF, 0xC1, 0x03, 0xFF, 0xF8, 0x7F, 0xFF, 0xFF, 0xFF, 0xC0, 0x07, 0xFF,
0xF0, 0x7F, 0xFF, 0xFF, 0xFF, 0xE0, 0x07, 0xFF, 0xF0, 0x3F, 0xFF, 0xFF, 0xFF, 0xE0, 0x07, 0xFF, 0xE0, 0x1F, 0xFF,
0xFF, 0xFF, 0xE0, 0x03, 0xFF, 0xE0, 0x1F, 0xFF, 0xFF, 0xFF, 0xE0, 0x03, 0xFF, 0xC0, 0x1F, 0xFF, 0xFF, 0xFF, 0xC0,
0x83, 0xFF, 0xC0, 0x1F, 0xFF, 0xFF, 0xFF, 0xC1, 0x81, 0xFF, 0xE0, 0x1F, 0xCF, 0xFF, 0xFF, 0xC1, 0x81, 0xFF, 0xF0,
0x3F, 0x87, 0xFF, 0xFF, 0x81, 0xC0, 0xFF, 0xF8, 0x7F, 0x03, 0xFF, 0xFF, 0x03, 0xC0, 0x7F, 0xFF, 0xFE, 0x03, 0xFF,
0xFE, 0x03, 0xE0, 0x3F, 0xFF, 0xFC, 0x01, 0xFF, 0xFC, 0x07, 0xF0, 0x0F, 0xFF, 0xFC, 0x00, 0xFF, 0xF0, 0x0F, 0xF8,
0x01, 0xFF, 0xF8, 0x00, 0xFF, 0x80, 0x0F, 0xFC, 0x01, 0xFF, 0xF0, 0x00, 0x7F, 0x80, 0x3F, 0xFE, 0x01, 0xFF, 0xF0,
0x00, 0x7F, 0x80, 0x7F, 0xFF, 0x81, 0xFF, 0xF0, 0x00, 0x3F, 0x80, 0xFF, 0xFF, 0xE1, 0xFF, 0xF0, 0x00, 0x3F, 0x87,
0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xF0, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x00,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
};
static const uint8_t WI48_RAIN[] = {
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE,
0x00, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x0F, 0x00, 0x1F, 0xFF, 0xFF, 0xFF,
0xFF, 0x00, 0xFF, 0xE0, 0x0F, 0xFF, 0xFF, 0xFF, 0xFE, 0x01, 0xFF, 0xF8, 0x0F, 0xFF, 0xFF, 0xFF, 0xFC, 0x07, 0xFF,
0xFC, 0x07, 0xFF, 0xFF, 0xFF, 0xFC, 0x0F, 0xFF, 0xFE, 0x03, 0xFF, 0xFF, 0xFF, 0xF8, 0x1F, 0xFF, 0xFF, 0x03, 0xFF,
0xFF, 0xFF, 0xF8, 0x1F, 0xFF, 0xFF, 0x81, 0xFF, 0xFF, 0xFF, 0xF0, 0x3F, 0xFF, 0xFF, 0xC1, 0xFF, 0xFF, 0xFF, 0xF0,
0x3F, 0xFF, 0xFF, 0xC1, 0xFF, 0xFF, 0xFF, 0xF0, 0x7F, 0xFF, 0xFF, 0xC0, 0x1F, 0xFF, 0xFF, 0x80, 0x7F, 0xFF, 0xFF,
0xE0, 0x01, 0xFF, 0xFE, 0x00, 0x7F, 0xFF, 0xFF, 0xE0, 0x00, 0x7F, 0xFC, 0x00, 0x7F, 0xFF, 0xFF, 0xE0, 0x00, 0x3F,
0xF8, 0x00, 0x7F, 0xFF, 0xFF, 0xE0, 0x00, 0x1F, 0xF0, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x0F, 0xE0, 0x1F, 0xFF,
0xFF, 0xFF, 0xFF, 0xF8, 0x07, 0xC0, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x03, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0x03, 0x81, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x81, 0x81, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x81, 0x83,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC1, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x03, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xE0, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xE0, 0x07, 0xFF, 0xFC, 0x3F, 0x8F, 0xE1, 0xFF, 0xE0, 0x03, 0xFF, 0xFC, 0x1F, 0x07, 0xC1, 0xFF, 0xC0, 0x03, 0xFF,
0xF8, 0x1F, 0x07, 0xC1, 0xFF, 0xC0, 0x83, 0xFF, 0xF8, 0x1F, 0x07, 0xC1, 0xFF, 0xC1, 0x81, 0xFF, 0xF8, 0x3E, 0x07,
0xC1, 0xFF, 0x81, 0x80, 0xFF, 0xF8, 0x3E, 0x0F, 0x81, 0xFF, 0x01, 0xC0, 0xFF, 0xF0, 0x3E, 0x0F, 0x83, 0xFF, 0x03,
0xE0, 0x3F, 0xF0, 0x3E, 0x0F, 0x83, 0xFC, 0x07, 0xE0, 0x1F, 0xF0, 0x7C, 0x0F, 0x03, 0xF8, 0x07, 0xF0, 0x01, 0xE0,
0x7C, 0x1F, 0x07, 0x80, 0x0F, 0xF8, 0x01, 0xE0, 0x7C, 0x1F, 0x07, 0x80, 0x1F, 0xFC, 0x01, 0xE0, 0xF8, 0x1F, 0x07,
0x80, 0x3F, 0xFF, 0x01, 0xE0, 0xF8, 0x1E, 0x07, 0x80, 0xFF, 0xFF, 0xC1, 0xC0, 0xF8, 0x3E, 0x0F, 0x83, 0xFF, 0xFF,
0xFD, 0xC0, 0xF8, 0x3E, 0x0F, 0xBF, 0xFF, 0xFF, 0xFF, 0xC1, 0xF0, 0x3E, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xC1, 0xF0,
0x7C, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0x81, 0xF0, 0x7C, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0x83, 0xE0, 0x7C, 0x1F, 0xFF,
0xFF, 0xFF, 0xFF, 0x83, 0xE0, 0x78, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0x83, 0xE0, 0xF8, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF,
0x87, 0xE0, 0xFC, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xEF, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC1, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0x81, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x83, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0x83, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x83, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x87, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
};
static const uint8_t WI48_SNOW[] = {
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE,
0x00, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x1F, 0x00, 0x1F, 0xFF, 0xFF, 0xFF,
0xFF, 0x00, 0xFF, 0xE0, 0x0F, 0xFF, 0xFF, 0xFF, 0xFE, 0x03, 0xFF, 0xF8, 0x0F, 0xFF, 0xFF, 0xFF, 0xFC, 0x07, 0xFF,
0xFC, 0x07, 0xFF, 0xFF, 0xFF, 0xFC, 0x0F, 0xFF, 0xFE, 0x03, 0xFF, 0xFF, 0xFF, 0xF8, 0x1F, 0xFF, 0xFF, 0x03, 0xFF,
0xFF, 0xFF, 0xF8, 0x1F, 0xFF, 0xFF, 0x81, 0xFF, 0xFF, 0xFF, 0xF0, 0x3F, 0xFF, 0xFF, 0x81, 0xFF, 0xFF, 0xFF, 0xF0,
0x3F, 0xFF, 0xFF, 0xC1, 0xFF, 0xFF, 0xFF, 0xF0, 0x7F, 0xFF, 0xFF, 0xC0, 0x1F, 0xFF, 0xFF, 0x80, 0x7F, 0xFF, 0xFF,
0xC0, 0x01, 0xFF, 0xFE, 0x00, 0x7F, 0xFF, 0xFF, 0xE0, 0x00, 0xFF, 0xFC, 0x00, 0x7F, 0xFF, 0xFF, 0xE0, 0x00, 0x3F,
0xF8, 0x00, 0x7F, 0xFF, 0xFF, 0xE0, 0x00, 0x1F, 0xF0, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x0F, 0xE0, 0x1F, 0xFF,
0xFF, 0xFF, 0xFF, 0xF8, 0x07, 0xC0, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x07, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0x03, 0x81, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x81, 0x81, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x81, 0x83,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC1, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC1, 0x07, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xC0, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xE0, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x03, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC1, 0x83, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC1, 0x81, 0xFF, 0xFF, 0xFE, 0x7F,
0xFF, 0xFF, 0x81, 0x81, 0xFF, 0xFF, 0xFC, 0x3F, 0xFF, 0xFF, 0x01, 0xC0, 0xFF, 0xFF, 0xFC, 0x1F, 0xFF, 0xFF, 0x03,
0xC0, 0x7F, 0xFF, 0xFC, 0x1F, 0xFF, 0xFC, 0x07, 0xE0, 0x1F, 0xFF, 0xFC, 0x3F, 0xFF, 0xF8, 0x07, 0xF0, 0x01, 0xFD,
0xFF, 0xFF, 0x9F, 0x80, 0x0F, 0xF8, 0x01, 0xF0, 0xFF, 0xFF, 0x0F, 0x80, 0x1F, 0xFC, 0x01, 0xF0, 0x7F, 0xFE, 0x07,
0x80, 0x3F, 0xFE, 0x01, 0xF0, 0x7F, 0xFE, 0x0F, 0x80, 0xFF, 0xFF, 0x81, 0xF0, 0xFF, 0xFF, 0x0F, 0x83, 0xFF, 0xFF,
0xFD, 0xFD, 0xFF, 0xFF, 0xFF, 0xBF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC,
0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x1F, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xF0, 0xFF, 0xFF, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x7F, 0xFE, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x7F, 0xFE,
0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0xFF, 0xFF, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFD, 0xFF, 0xFF, 0x9F, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFC, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x3F, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
};
static const uint8_t WI48_THUNDERSTORM[] = {
0xFF, 0xFF, 0xFF, 0xC0, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8,
0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x3F,
0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x1F, 0x80, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xF0, 0x0F, 0xFF, 0xFF, 0xFF,
0xFE, 0x01, 0xFF, 0xF8, 0x07, 0xFF, 0xFF, 0xFF, 0xFC, 0x07, 0xFF, 0xFE, 0x07, 0xFF, 0xFF, 0xFF, 0xFC, 0x0F, 0xFF,
0xFF, 0x03, 0xFF, 0xFF, 0xFF, 0xF8, 0x1F, 0xFF, 0xFF, 0x03, 0xFF, 0xFF, 0xFF, 0xF8, 0x1F, 0xFF, 0xFF, 0x81, 0xFF,
0xFF, 0xFF, 0xF0, 0x3F, 0xFF, 0xFF, 0xC1, 0xFF, 0xFF, 0xFF, 0xF0, 0x3F, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xE0,
0x7F, 0xFF, 0xFF, 0xC0, 0x07, 0xFF, 0xFF, 0x80, 0x7F, 0xFF, 0xFF, 0xE0, 0x01, 0xFF, 0xFE, 0x00, 0x7F, 0xFF, 0xFF,
0xE0, 0x00, 0x7F, 0xFC, 0x00, 0x7F, 0xFF, 0xFF, 0xE0, 0x00, 0x3F, 0xF8, 0x00, 0x7F, 0xFF, 0xFF, 0xE0, 0x00, 0x1F,
0xF0, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x0F, 0xE0, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x07, 0xC0, 0x7F, 0xFF,
0xFF, 0xFF, 0xFF, 0xFE, 0x03, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0x81, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0x81, 0x81, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x81, 0x83, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC1, 0x03,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x07, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xE0, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x03, 0xFF, 0x00, 0x3F, 0x87, 0xE1, 0xFF,
0xE0, 0x03, 0xFF, 0x00, 0x3F, 0x07, 0xE1, 0xFF, 0xC0, 0x03, 0xFE, 0x00, 0x7F, 0x07, 0xC0, 0xFF, 0xC0, 0x83, 0xFE,
0x00, 0x7F, 0x07, 0xC1, 0xFF, 0xC1, 0x81, 0xFE, 0x00, 0xFE, 0x07, 0xC1, 0xFF, 0x81, 0xC0, 0xFC, 0x00, 0xFE, 0x0F,
0x81, 0xFF, 0x01, 0xC0, 0x7C, 0x01, 0xFE, 0x0F, 0x81, 0xFE, 0x03, 0xE0, 0x3C, 0x01, 0xFE, 0x0F, 0x83, 0xFC, 0x07,
0xE0, 0x08, 0x03, 0xFC, 0x0F, 0x83, 0xF0, 0x07, 0xF0, 0x00, 0x03, 0xFC, 0x1F, 0x03, 0x80, 0x0F, 0xF8, 0x00, 0x07,
0xFC, 0x1F, 0x07, 0x80, 0x1F, 0xFC, 0x00, 0x07, 0xFC, 0x1F, 0x07, 0x80, 0x3F, 0xFF, 0x00, 0x0F, 0xF8, 0x1F, 0x07,
0x80, 0xFF, 0xFF, 0xC0, 0x1F, 0xF8, 0x3E, 0x07, 0x83, 0xFF, 0xFF, 0xE0, 0x1F, 0xF8, 0x3E, 0x0F, 0xFF, 0xFF, 0xFF,
0xC0, 0x00, 0x38, 0x3E, 0x0F, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x70, 0x3E, 0x0F, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0xF0,
0x7C, 0x0F, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0xF0, 0x7C, 0x1F, 0xFF, 0xFF, 0xFF, 0x80, 0x01, 0xE0, 0x7C, 0x1F, 0xFF,
0xFF, 0xFF, 0xFF, 0x03, 0xE0, 0x7C, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0xE0, 0xFE, 0x3F, 0xFF, 0xFF, 0xFF, 0xFE,
0x07, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x0F, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x0F, 0xC0, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x1F, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x3F, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFC, 0x3F, 0x81, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x7F, 0x83, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0xFF,
0x83, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0xFF, 0xC3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF9, 0xFF, 0xEF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFB, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
};
+96
View File
@@ -0,0 +1,96 @@
#include "WeatherSettingsStore.h"
#include <ArduinoJson.h>
#include <HalStorage.h>
#include <Logging.h>
WeatherSettingsStore WeatherSettingsStore::instance;
namespace {
constexpr char WEATHER_SETTINGS_FILE[] = "/.crosspoint/weather_settings.json";
}
bool WeatherSettingsStore::saveToFile() const {
Storage.mkdir("/.crosspoint");
JsonDocument doc;
doc["latitude"] = latitude;
doc["longitude"] = longitude;
doc["locationName"] = locationName;
doc["tempUnit"] = static_cast<uint8_t>(tempUnit);
doc["windUnit"] = static_cast<uint8_t>(windUnit);
doc["precipUnit"] = static_cast<uint8_t>(precipUnit);
doc["forecastDays"] = forecastDays;
String json;
serializeJson(doc, json);
return Storage.writeFile(WEATHER_SETTINGS_FILE, json);
}
bool WeatherSettingsStore::loadFromFile() {
if (!Storage.exists(WEATHER_SETTINGS_FILE)) {
LOG_DBG("WEA", "No weather settings file found");
return false;
}
String json = Storage.readFile(WEATHER_SETTINGS_FILE);
if (json.isEmpty()) {
return false;
}
JsonDocument doc;
auto error = deserializeJson(doc, json);
if (error) {
LOG_ERR("WEA", "JSON parse error: %s", error.c_str());
return false;
}
latitude = doc["latitude"] | 0.0f;
longitude = doc["longitude"] | 0.0f;
locationName = doc["locationName"] | std::string("");
tempUnit = static_cast<WeatherTempUnit>(doc["tempUnit"] | (uint8_t)0);
windUnit = static_cast<WeatherWindUnit>(doc["windUnit"] | (uint8_t)0);
precipUnit = static_cast<WeatherPrecipUnit>(doc["precipUnit"] | (uint8_t)0);
forecastDays = doc["forecastDays"] | (uint8_t)3;
if (forecastDays < 1) forecastDays = 1;
if (forecastDays > 5) forecastDays = 5;
LOG_DBG("WEA", "Loaded weather settings: %s (%.2f, %.2f)", locationName.c_str(), latitude, longitude);
return true;
}
void WeatherSettingsStore::setLocation(float lat, float lon, const std::string& name) {
latitude = lat;
longitude = lon;
locationName = name;
LOG_DBG("WEA", "Set location: %s (%.4f, %.4f)", name.c_str(), lat, lon);
}
void WeatherSettingsStore::setForecastDays(uint8_t days) {
if (days < 1) days = 1;
if (days > 5) days = 5;
forecastDays = days;
}
const char* WeatherSettingsStore::getTempUnitParam() const {
return tempUnit == WeatherTempUnit::FAHRENHEIT ? "fahrenheit" : "celsius";
}
const char* WeatherSettingsStore::getWindUnitParam() const {
switch (windUnit) {
case WeatherWindUnit::MS:
return "ms";
case WeatherWindUnit::MPH:
return "mph";
case WeatherWindUnit::KNOTS:
return "kn";
case WeatherWindUnit::KMH:
default:
return "kmh";
}
}
const char* WeatherSettingsStore::getPrecipUnitParam() const {
return precipUnit == WeatherPrecipUnit::INCH ? "inch" : "mm";
}
+57
View File
@@ -0,0 +1,57 @@
#pragma once
#include <cstdint>
#include <string>
enum class WeatherTempUnit : uint8_t { CELSIUS = 0, FAHRENHEIT = 1 };
enum class WeatherWindUnit : uint8_t { KMH = 0, MS = 1, MPH = 2, KNOTS = 3 };
enum class WeatherPrecipUnit : uint8_t { MM = 0, INCH = 1 };
class WeatherSettingsStore {
private:
static WeatherSettingsStore instance;
float latitude = 0;
float longitude = 0;
std::string locationName;
WeatherTempUnit tempUnit = WeatherTempUnit::CELSIUS;
WeatherWindUnit windUnit = WeatherWindUnit::KMH;
WeatherPrecipUnit precipUnit = WeatherPrecipUnit::MM;
uint8_t forecastDays = 3;
WeatherSettingsStore() = default;
public:
WeatherSettingsStore(const WeatherSettingsStore&) = delete;
WeatherSettingsStore& operator=(const WeatherSettingsStore&) = delete;
static WeatherSettingsStore& getInstance() { return instance; }
bool saveToFile() const;
bool loadFromFile();
// Location
void setLocation(float lat, float lon, const std::string& name);
float getLatitude() const { return latitude; }
float getLongitude() const { return longitude; }
const std::string& getLocationName() const { return locationName; }
bool hasLocation() const { return latitude != 0 || longitude != 0; }
// Units
void setTempUnit(WeatherTempUnit unit) { tempUnit = unit; }
WeatherTempUnit getTempUnit() const { return tempUnit; }
void setWindUnit(WeatherWindUnit unit) { windUnit = unit; }
WeatherWindUnit getWindUnit() const { return windUnit; }
void setPrecipUnit(WeatherPrecipUnit unit) { precipUnit = unit; }
WeatherPrecipUnit getPrecipUnit() const { return precipUnit; }
// Forecast
void setForecastDays(uint8_t days);
uint8_t getForecastDays() const { return forecastDays; }
// API parameter strings
const char* getTempUnitParam() const;
const char* getWindUnitParam() const;
const char* getPrecipUnitParam() const;
};
#define WEATHER_SETTINGS WeatherSettingsStore::getInstance()
+103 -24
View File
@@ -3,36 +3,113 @@ import os
from PIL import Image
import cairosvg
import io
import xml.etree.ElementTree as ET
threshold = 128
def parse_svg_intrinsic_size(svg_data):
try:
root = ET.fromstring(svg_data)
except ET.ParseError:
return None, None
viewbox = root.get("viewBox") or root.get("viewbox")
if viewbox:
parts = viewbox.replace(",", " ").split()
if len(parts) == 4:
try:
vb_w = float(parts[2])
vb_h = float(parts[3])
if vb_w > 0 and vb_h > 0:
return vb_w, vb_h
except ValueError:
pass
def parse_len(value):
if not value:
return None
cleaned = "".join(ch for ch in value if ch.isdigit() or ch in ".-")
if not cleaned:
return None
try:
parsed = float(cleaned)
return parsed if parsed > 0 else None
except ValueError:
return None
w = parse_len(root.get("width"))
h = parse_len(root.get("height"))
return w, h
def fit_inside_canvas(src_w, src_h, dst_w, dst_h):
if src_w <= 0 or src_h <= 0 or dst_w <= 0 or dst_h <= 0:
return dst_w, dst_h
src_ratio = src_w / src_h
dst_ratio = dst_w / dst_h
if src_ratio >= dst_ratio:
fit_w = dst_w
fit_h = max(1, int(round(fit_w / src_ratio)))
else:
fit_h = dst_h
fit_w = max(1, int(round(fit_h * src_ratio)))
return fit_w, fit_h
def svg_to_png_bytes(svg_path, width, height):
with open(svg_path, 'rb') as f:
with open(svg_path, "rb") as f:
svg_data = f.read()
png_bytes = cairosvg.svg2png(bytestring=svg_data, output_width=width, output_height=height)
src_w, src_h = parse_svg_intrinsic_size(svg_data)
render_w, render_h = fit_inside_canvas(
src_w or width, src_h or height, width, height
)
png_bytes = cairosvg.svg2png(
bytestring=svg_data, output_width=render_w, output_height=render_h
)
return png_bytes
def center_on_canvas(img, width, height):
if img.mode != "RGBA":
img = img.convert("RGBA")
canvas = Image.new("RGBA", (width, height), (255, 255, 255, 255))
x = (width - img.width) // 2
y = (height - img.height) // 2
canvas.paste(img, (x, y), img)
return canvas
def load_image(path, width, height):
ext = os.path.splitext(path)[1].lower()
if ext == '.svg':
if ext == ".svg":
png_bytes = svg_to_png_bytes(path, width, height)
img = Image.open(io.BytesIO(png_bytes))
img = Image.open(io.BytesIO(png_bytes)).convert("RGBA")
img = center_on_canvas(img, width, height)
else:
img = Image.open(path)
img = img.convert('RGBA')
img = img.resize((width, height), Image.LANCZOS)
img = Image.open(path).convert("RGBA")
# Keep source aspect ratio and fit inside requested canvas.
fit = img.copy()
fit.thumbnail((width, height), Image.LANCZOS)
img = center_on_canvas(fit, width, height)
# Flatten alpha: paste on white background
background = Image.new('RGBA', img.size, (255, 255, 255, 255))
background = Image.new("RGBA", img.size, (255, 255, 255, 255))
background.paste(img, mask=img.split()[3])
img = background
# Rotate 90 degrees counterclockwise
img = img.rotate(90, expand=True)
return img
def image_to_c_array(img, array_name):
# Convert to grayscale, then threshold to get white=1, black=0
# Convert to grayscale
img = img.convert('L')
img = img.convert("L")
width, height = img.size
pixels = list(img.getdata())
packed = []
@@ -44,37 +121,39 @@ def image_to_c_array(img, array_name):
v = pixels[y * width + x + b]
# 1 for white, 0 for black
bit = 1 if v >= threshold else 0
byte |= (bit << (7 - b))
byte |= bit << (7 - b)
packed.append(byte)
# Format as C array
c = f'#pragma once\n#include <cstdint>\n\n'
c += f'// size: {width}x{height}\n'
c += f'static const uint8_t {array_name}[] = {{\n '
c = f"#pragma once\n#include <cstdint>\n\n"
c += f"// size: {width}x{height}\n"
c += f"static const uint8_t {array_name}[] = {{\n "
for i, v in enumerate(packed):
c += f'0x{v:02X}, '
c += f"0x{v:02X}, "
if (i + 1) % 16 == 0:
c += '\n '
c = c.rstrip(', \n') + '\n};\n'
c += "\n "
c = c.rstrip(", \n") + "\n};\n"
return c
def main():
if len(sys.argv) < 5:
print('Usage: python convert_image.py input.png output_name width height')
print("Usage: python convert_image.py input.png output_name width height")
sys.exit(1)
input_path, output_name, width, height = sys.argv[1:5]
array_name = output_name.capitalize() + 'Icon'
array_name = output_name.capitalize() + "Icon"
width, height = int(width), int(height)
img = load_image(input_path, width, height)
c_array = image_to_c_array(img, array_name)
# Always save to src/components/icons/[output_name].h relative to project root
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
output_dir = os.path.join(project_root, 'src', 'components', 'icons')
output_dir = os.path.join(project_root, "src", "components", "icons")
os.makedirs(output_dir, exist_ok=True)
output_path = os.path.join(output_dir, f'{output_name}.h')
with open(output_path, 'w') as f:
output_path = os.path.join(output_dir, f"{output_name}.h")
with open(output_path, "w") as f:
f.write(c_array)
print(f'Wrote {output_path}')
print(f"Wrote {output_path}")
if __name__ == '__main__':
main()
if __name__ == "__main__":
main()
+280
View File
@@ -0,0 +1,280 @@
#!/usr/bin/env python3
"""
Generate lib/Weather/WeatherIcons48.h from SVG sources.
By default, this script loads SVG files from assets/weather-icons/svg.
Use --fetch to download missing files from erikflowers/weather-icons.
"""
import argparse
import io
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
import urllib.request
import xml.etree.ElementTree as ET
import zipfile
from PIL import Image
try:
import cairosvg # type: ignore
except Exception:
cairosvg = None
SIZE = 64
# Higher threshold slightly thickens dark icon strokes after antialiasing.
THRESHOLD = 160
# Keep zero margin so rendered glyphs can use the full 48x48 canvas.
CONTENT_MARGIN = 0
PROJECT_ROOT = Path(__file__).resolve().parent.parent
DEFAULT_SVG_DIR = PROJECT_ROOT / "assets" / "weather-icons" / "svg"
DEFAULT_OUT = PROJECT_ROOT / "lib" / "Weather" / "WeatherIcons48.h"
UPSTREAM_BASE = "https://raw.githubusercontent.com/erikflowers/weather-icons/master/svg"
RESVG_ZIP_URL = (
"https://github.com/linebender/resvg/releases/latest/download/resvg-win64.zip"
)
RESVG_EXE = PROJECT_ROOT / ".cache" / "resvg" / "resvg.exe"
ICON_SOURCES = {
"WI48_CLEAR_DAY": "wi-day-sunny.svg",
"WI48_CLEAR_NIGHT": "wi-night-clear.svg",
"WI48_PARTLY_CLOUDY_DAY": "wi-day-cloudy.svg",
"WI48_PARTLY_CLOUDY_NIGHT": "wi-night-alt-cloudy.svg",
"WI48_OVERCAST": "wi-cloudy.svg",
"WI48_FOG": "wi-fog.svg",
"WI48_DRIZZLE": "wi-sprinkle.svg",
"WI48_RAIN": "wi-rain.svg",
"WI48_SNOW": "wi-snow.svg",
"WI48_THUNDERSTORM": "wi-thunderstorm.svg",
}
def parse_svg_intrinsic_size(svg_data):
try:
root = ET.fromstring(svg_data)
except ET.ParseError:
return None, None
viewbox = root.get("viewBox") or root.get("viewbox")
if viewbox:
parts = viewbox.replace(",", " ").split()
if len(parts) == 4:
try:
vb_w = float(parts[2])
vb_h = float(parts[3])
if vb_w > 0 and vb_h > 0:
return vb_w, vb_h
except ValueError:
pass
def parse_len(value):
if not value:
return None
cleaned = "".join(ch for ch in value if ch.isdigit() or ch in ".-")
if not cleaned:
return None
try:
parsed = float(cleaned)
return parsed if parsed > 0 else None
except ValueError:
return None
return parse_len(root.get("width")), parse_len(root.get("height"))
def fit_inside_canvas(src_w, src_h, dst_w, dst_h):
if src_w <= 0 or src_h <= 0:
return dst_w, dst_h
src_ratio = src_w / src_h
dst_ratio = dst_w / dst_h
if src_ratio >= dst_ratio:
fit_w = dst_w
fit_h = max(1, int(round(fit_w / src_ratio)))
else:
fit_h = dst_h
fit_w = max(1, int(round(fit_h * src_ratio)))
return fit_w, fit_h
def ensure_resvg_binary():
if shutil.which("resvg"):
return Path(shutil.which("resvg"))
if RESVG_EXE.exists():
return RESVG_EXE
RESVG_EXE.parent.mkdir(parents=True, exist_ok=True)
archive_path = RESVG_EXE.parent / "resvg.zip"
with urllib.request.urlopen(RESVG_ZIP_URL) as response:
archive_path.write_bytes(response.read())
with zipfile.ZipFile(archive_path, "r") as zf:
zf.extractall(RESVG_EXE.parent)
found = list(RESVG_EXE.parent.rglob("resvg.exe"))
if not found:
raise RuntimeError("resvg.exe not found after extraction")
if found[0] != RESVG_EXE:
RESVG_EXE.write_bytes(found[0].read_bytes())
return RESVG_EXE
def render_svg_with_resvg(svg_data, render_w, render_h):
exe = ensure_resvg_binary()
with tempfile.TemporaryDirectory() as tmp:
tmp_path = Path(tmp)
in_svg = tmp_path / "icon.svg"
out_png = tmp_path / "icon.png"
in_svg.write_bytes(svg_data)
cmd = [
str(exe),
"--width",
str(render_w),
"--height",
str(render_h),
str(in_svg),
str(out_png),
]
subprocess.run(cmd, check=True, capture_output=True)
return out_png.read_bytes()
def render_svg_contain(svg_data, width, height):
src_w, src_h = parse_svg_intrinsic_size(svg_data)
render_w, render_h = fit_inside_canvas(
src_w or width, src_h or height, width, height
)
# Render larger first so trimming and re-fit preserve detail quality.
oversample = 4
render_w *= oversample
render_h *= oversample
if cairosvg is not None:
png_bytes = cairosvg.svg2png(
bytestring=svg_data, output_width=render_w, output_height=render_h
)
else:
png_bytes = render_svg_with_resvg(svg_data, render_w, render_h)
icon = Image.open(io.BytesIO(png_bytes)).convert("RGBA")
# Trim transparent/empty margins so symbols use available icon area better.
alpha_bbox = icon.split()[3].getbbox()
if alpha_bbox is not None:
icon = icon.crop(alpha_bbox)
max_w = max(1, width - 2 * CONTENT_MARGIN)
max_h = max(1, height - 2 * CONTENT_MARGIN)
icon.thumbnail((max_w, max_h), Image.Resampling.LANCZOS)
canvas = Image.new("RGBA", (width, height), (255, 255, 255, 255))
off_x = (width - icon.width) // 2
off_y = (height - icon.height) // 2
canvas.paste(icon, (off_x, off_y), icon)
# Flatten alpha on white and convert to monochrome-friendly grayscale.
flat = Image.new("RGBA", canvas.size, (255, 255, 255, 255))
flat.paste(canvas, mask=canvas.split()[3])
return flat.convert("L")
def image_to_packed_bits(img):
width, height = img.size
pixels = img.tobytes()
packed = []
for y in range(height):
for x in range(0, width, 8):
out = 0
for b in range(8):
px = x + b
lum = pixels[y * width + px] if px < width else 255
# 1-bit means white/clear (not drawn), 0-bit means black/drawn.
bit = 1 if lum >= THRESHOLD else 0
out |= bit << (7 - b)
packed.append(out)
return packed
def format_array(name, data):
lines = []
per_line = 16
for i in range(0, len(data), per_line):
chunk = data[i : i + per_line]
lines.append(" " + ", ".join(f"0x{v:02X}" for v in chunk) + ",")
body = "\n".join(lines)
return f"static const uint8_t {name}[] = {{\n{body}\n}};\n"
def ensure_svg(path, fetch):
if path.exists():
return
if not fetch:
raise FileNotFoundError(f"Missing SVG: {path}")
path.parent.mkdir(parents=True, exist_ok=True)
url = f"{UPSTREAM_BASE}/{path.name}"
with urllib.request.urlopen(url) as response:
data = response.read()
path.write_bytes(data)
def generate(svg_dir, output_path, fetch):
arrays = []
for symbol, filename in ICON_SOURCES.items():
svg_path = svg_dir / filename
ensure_svg(svg_path, fetch)
svg_data = svg_path.read_bytes()
img = render_svg_contain(svg_data, SIZE, SIZE)
packed = image_to_packed_bits(img)
arrays.append(format_array(symbol, packed))
header = [
"#pragma once",
"#include <cstdint>",
"",
"// Generated from erikflowers/weather-icons SVGs.",
"// 48x48, 1-bit, MSB-first, row-major.",
"// Regenerate with: python scripts/generate_weather_icons.py --fetch",
"// clang-format off",
"",
]
header.extend(arrays)
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text("\n".join(header) + "\n", encoding="utf-8")
def main():
parser = argparse.ArgumentParser(
description="Generate WeatherIcons48.h from SVG files"
)
parser.add_argument(
"--svg-dir",
type=Path,
default=DEFAULT_SVG_DIR,
help="Directory containing source SVG files",
)
parser.add_argument(
"--output", type=Path, default=DEFAULT_OUT, help="Output header path"
)
parser.add_argument(
"--fetch", action="store_true", help="Fetch missing SVG files from upstream"
)
args = parser.parse_args()
generate(args.svg_dir, args.output, args.fetch)
rel_out = os.path.relpath(args.output, PROJECT_ROOT)
print(f"Wrote {rel_out}")
if __name__ == "__main__":
main()
+3
View File
@@ -14,6 +14,7 @@
#include "reader/ReaderActivity.h"
#include "settings/SettingsActivity.h"
#include "util/FullScreenMessageActivity.h"
#include "weather/WeatherActivity.h"
void ActivityManager::begin() {
xTaskCreate(&renderTaskTrampoline, "ActivityManagerRender",
@@ -210,6 +211,8 @@ void ActivityManager::goToFullScreenMessage(std::string message, EpdFontFamily::
replaceActivity(std::make_unique<FullScreenMessageActivity>(renderer, mappedInput, std::move(message), style));
}
void ActivityManager::goToWeather() { replaceActivity(std::make_unique<WeatherActivity>(renderer, mappedInput)); }
void ActivityManager::goHome() { replaceActivity(std::make_unique<HomeActivity>(renderer, mappedInput)); }
void ActivityManager::pushActivity(std::unique_ptr<Activity>&& activity) {
+1
View File
@@ -86,6 +86,7 @@ class ActivityManager {
void goToSleep();
void goToBoot();
void goToFullScreenMessage(std::string message, EpdFontFamily::Style style = EpdFontFamily::REGULAR);
void goToWeather();
void goHome();
// This will move current activity to stack instead of deleting it
+9 -4
View File
@@ -20,7 +20,7 @@
#include "fontIds.h"
int HomeActivity::getMenuItemCount() const {
int count = 4; // File Browser, Recents, File transfer, Settings
int count = 5; // File Browser, Recents, File transfer, Weather, Settings
if (!recentBooks.empty()) {
count += recentBooks.size();
}
@@ -192,6 +192,7 @@ void HomeActivity::loop() {
const int recentsIdx = idx++;
const int opdsLibraryIdx = hasOpdsUrl ? idx++ : -1;
const int fileTransferIdx = idx++;
const int weatherIdx = idx++;
const int settingsIdx = idx;
if (selectorIndex < recentBooks.size()) {
@@ -202,6 +203,8 @@ void HomeActivity::loop() {
onRecentsOpen();
} else if (menuSelectedIndex == opdsLibraryIdx) {
onOpdsBrowserOpen();
} else if (menuSelectedIndex == weatherIdx) {
onWeatherOpen();
} else if (menuSelectedIndex == fileTransferIdx) {
onFileTransferOpen();
} else if (menuSelectedIndex == settingsIdx) {
@@ -226,11 +229,11 @@ void HomeActivity::render(RenderLock&&) {
// Build menu items dynamically
std::vector<const char*> menuItems = {tr(STR_BROWSE_FILES), tr(STR_MENU_RECENT_BOOKS), tr(STR_FILE_TRANSFER),
tr(STR_SETTINGS_TITLE)};
std::vector<UIIcon> menuIcons = {Folder, Recent, Transfer, Settings};
tr(STR_WEATHER), tr(STR_SETTINGS_TITLE)};
std::vector<UIIcon> menuIcons = {Folder, Recent, Library, Transfer, Settings};
if (hasOpdsUrl) {
// Insert OPDS Browser after File Browser
// Insert OPDS Browser after Recents (before File Transfer)
menuItems.insert(menuItems.begin() + 2, tr(STR_OPDS_BROWSER));
menuIcons.insert(menuIcons.begin() + 2, Library);
}
@@ -269,3 +272,5 @@ void HomeActivity::onSettingsOpen() { activityManager.goToSettings(); }
void HomeActivity::onFileTransferOpen() { activityManager.goToFileTransfer(); }
void HomeActivity::onOpdsBrowserOpen() { activityManager.goToBrowser(); }
void HomeActivity::onWeatherOpen() { activityManager.goToWeather(); }
+1
View File
@@ -26,6 +26,7 @@ class HomeActivity final : public Activity {
void onSettingsOpen();
void onFileTransferOpen();
void onOpdsBrowserOpen();
void onWeatherOpen();
int getMenuItemCount() const;
bool storeCoverBuffer(); // Store frame buffer for cover image
@@ -19,6 +19,7 @@
#include "SyncTimeActivity.h"
#include "SystemInformationActivity.h"
#include "activities/network/WifiSelectionActivity.h"
#include "activities/weather/WeatherSettingsActivity.h"
#include "components/UITheme.h"
#include "fontIds.h"
@@ -64,6 +65,7 @@ void SettingsActivity::onEnter() {
systemSettings.push_back(SettingInfo::Action(StrId::STR_CHECK_UPDATES, SettingAction::CheckForUpdates));
systemSettings.push_back(SettingInfo::Action(StrId::STR_LANGUAGE, SettingAction::Language));
systemSettings.push_back(SettingInfo::Action(StrId::STR_SYSTEM_INFO, SettingAction::SystemInfo));
systemSettings.push_back(SettingInfo::Action(StrId::STR_WEATHER_SETTINGS, SettingAction::Weather));
readerSettings.push_back(SettingInfo::Action(StrId::STR_CUSTOMISE_STATUS_BAR, SettingAction::CustomiseStatusBar));
// Reset selection to first category
@@ -207,6 +209,9 @@ void SettingsActivity::toggleCurrentSetting() {
case SettingAction::Language:
startActivityForResult(std::make_unique<LanguageSelectActivity>(renderer, mappedInput), resultHandler);
break;
case SettingAction::Weather:
startActivityForResult(std::make_unique<WeatherSettingsActivity>(renderer, mappedInput), resultHandler);
break;
case SettingAction::SystemInfo:
startActivityForResult(std::make_unique<SystemInformationActivity>(renderer, mappedInput), resultHandler);
break;
@@ -25,6 +25,7 @@ enum class SettingAction {
SystemInfo,
DetectTimezone,
SyncTime,
Weather,
};
struct SettingInfo {
+743
View File
@@ -0,0 +1,743 @@
#include "WeatherActivity.h"
#include <GfxRenderer.h>
#include <I18n.h>
#include <Logging.h>
#include <WeatherClient.h>
#include <WeatherIcons.h>
#include <WeatherSettingsStore.h>
#include <WiFi.h>
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <ctime>
#include <vector>
#include "MappedInputManager.h"
#include "WeatherSettingsActivity.h"
#include "activities/network/WifiSelectionActivity.h"
#include "components/UITheme.h"
#include "fontIds.h"
namespace {
inline bool getBitmapBit(const uint8_t* bitmap, const int size, const int x, const int y) {
const int rowBytes = size / 8;
const int idx = y * rowBytes + (x / 8);
const uint8_t mask = static_cast<uint8_t>(0x80 >> (x % 8));
return (bitmap[idx] & mask) != 0;
}
void drawWeatherIconWithOrientation(const GfxRenderer& renderer, const uint8_t* icon, const int x, const int y,
const int size) {
for (int srcY = 0; srcY < size; srcY++) {
for (int srcX = 0; srcX < size; srcX++) {
// 0 bits are black (drawn) in these icon bitmaps.
if (getBitmapBit(icon, size, srcX, srcY)) {
continue;
}
renderer.drawPixel(x + srcX, y + srcY);
}
}
}
StrId getWeatherDescriptionStrId(const int wmoCode) {
switch (wmoCode) {
case 0:
return StrId::STR_WEATHER_DESC_CLEAR_SKY;
case 1:
return StrId::STR_WEATHER_DESC_MAINLY_CLEAR;
case 2:
return StrId::STR_WEATHER_DESC_PARTLY_CLOUDY;
case 3:
return StrId::STR_WEATHER_DESC_OVERCAST;
case 45:
return StrId::STR_WEATHER_DESC_FOG;
case 48:
return StrId::STR_WEATHER_DESC_RIME_FOG;
case 51:
return StrId::STR_WEATHER_DESC_LIGHT_DRIZZLE;
case 53:
return StrId::STR_WEATHER_DESC_DRIZZLE;
case 55:
return StrId::STR_WEATHER_DESC_DENSE_DRIZZLE;
case 56:
return StrId::STR_WEATHER_DESC_FREEZING_DRIZZLE;
case 57:
return StrId::STR_WEATHER_DESC_DENSE_FREEZING_DRIZZLE;
case 61:
return StrId::STR_WEATHER_DESC_SLIGHT_RAIN;
case 63:
return StrId::STR_WEATHER_DESC_MODERATE_RAIN;
case 65:
return StrId::STR_WEATHER_DESC_HEAVY_RAIN;
case 66:
return StrId::STR_WEATHER_DESC_FREEZING_RAIN;
case 67:
return StrId::STR_WEATHER_DESC_HEAVY_FREEZING_RAIN;
case 71:
return StrId::STR_WEATHER_DESC_SLIGHT_SNOW;
case 73:
return StrId::STR_WEATHER_DESC_MODERATE_SNOW;
case 75:
return StrId::STR_WEATHER_DESC_HEAVY_SNOW;
case 77:
return StrId::STR_WEATHER_DESC_SNOW_GRAINS;
case 80:
return StrId::STR_WEATHER_DESC_SLIGHT_SHOWERS;
case 81:
return StrId::STR_WEATHER_DESC_MODERATE_SHOWERS;
case 82:
return StrId::STR_WEATHER_DESC_VIOLENT_SHOWERS;
case 85:
return StrId::STR_WEATHER_DESC_SNOW_SHOWERS;
case 86:
return StrId::STR_WEATHER_DESC_HEAVY_SNOW_SHOWERS;
case 95:
return StrId::STR_WEATHER_DESC_THUNDERSTORM;
case 96:
return StrId::STR_WEATHER_DESC_THUNDERSTORM_HAIL;
case 99:
return StrId::STR_WEATHER_DESC_THUNDERSTORM_HEAVY_HAIL;
default:
return StrId::STR_WEATHER_DESC_UNKNOWN;
}
}
} // namespace
void WeatherActivity::onEnter() {
Activity::onEnter();
// Force landscape orientation for weather display
renderer.setOrientation(GfxRenderer::Orientation::LandscapeClockwise);
state = State::LOADING_CACHE;
errorMessage.clear();
forceRefresh = false;
showRefreshPopup = false;
requestUpdate();
loadAndDisplay();
}
void WeatherActivity::onExit() {
Activity::onExit();
// Restore orientation from settings
renderer.setOrientation(static_cast<GfxRenderer::Orientation>(SETTINGS.orientation));
WiFi.mode(WIFI_OFF);
}
void WeatherActivity::loadAndDisplay() {
constexpr long CACHE_REFRESH_AGE_SECONDS = 30L * 60L;
if (!WEATHER_SETTINGS.hasLocation()) {
state = State::ERROR;
errorMessage = tr(STR_WEATHER_NO_LOCATION);
requestUpdate();
return;
}
// Try cache first
WeatherData cached = WeatherClient::getWeather(WEATHER_SETTINGS, forceRefresh);
if (cached.valid) {
const time_t now = time(nullptr);
const long age = (cached.fetchedAt > 0) ? static_cast<long>(now - cached.fetchedAt) : -1;
const bool cacheIsStale = (age >= 0 && age > CACHE_REFRESH_AGE_SECONDS) || age < 0;
const bool shouldBackgroundRefresh = forceRefresh || cacheIsStale;
if (cacheIsStale) {
LOG_DBG("WEA", "Displayed stale cache (age=%ld s); scheduling refresh", age);
} else {
LOG_DBG("WEA", "Displayed fresh cache (age=%ld s); skip auto-refresh", age);
}
weatherData = std::move(cached);
state = State::WEATHER_DISPLAY;
requestUpdate();
// Refresh only when forced or when cache is stale.
if (shouldBackgroundRefresh) {
checkAndConnectWifi();
}
return;
}
// Need fresh data - check WiFi
LOG_DBG("WEA", "No cache to display; proceeding to WiFi/network fetch path");
checkAndConnectWifi();
}
void WeatherActivity::checkAndConnectWifi() {
if (WiFi.status() == WL_CONNECTED && WiFi.localIP() != IPAddress(0, 0, 0, 0)) {
state = State::FETCHING;
requestUpdate(true);
fetchWeather();
return;
}
launchWifiSelection();
}
void WeatherActivity::launchWifiSelection() {
state = State::WIFI_SELECTION;
requestUpdate();
startActivityForResult(std::make_unique<WifiSelectionActivity>(renderer, mappedInput),
[this](const ActivityResult& result) { onWifiSelectionComplete(!result.isCancelled); });
}
void WeatherActivity::onWifiSelectionComplete(bool connected) {
// Re-apply landscape after returning from WiFi selection (which uses portrait)
renderer.setOrientation(GfxRenderer::Orientation::LandscapeClockwise);
LOG_DBG("WEA", "onWifiSelectionComplete connected=%d wifiStatus=%d", connected ? 1 : 0, (int)WiFi.status());
if (connected) {
state = State::CHECK_WIFI;
wifiWaitStartedAtMs = millis();
LOG_DBG("WEA", "state -> CHECK_WIFI (waitStart=%lu)", (unsigned long)wifiWaitStartedAtMs);
requestUpdate(true);
} else {
WiFi.disconnect();
WiFi.mode(WIFI_OFF);
state = State::ERROR;
errorMessage = tr(STR_WIFI_CONN_FAILED);
requestUpdate();
}
}
void WeatherActivity::fetchWeather() {
LOG_DBG("WEA", "fetchWeather[1] enter status=%d ip=%s", (int)WiFi.status(), WiFi.localIP().toString().c_str());
if (WiFi.status() != WL_CONNECTED || WiFi.localIP() == IPAddress(0, 0, 0, 0)) {
LOG_ERR("WEA", "fetchWeather[2] WiFi not ready status=%d ip=%s", (int)WiFi.status(),
WiFi.localIP().toString().c_str());
state = State::ERROR;
errorMessage = tr(STR_WIFI_CONN_FAILED);
requestUpdate();
return;
}
LOG_DBG("WEA", "fetchWeather[3] WeatherClient::getWeather before");
weatherData = WeatherClient::getWeather(WEATHER_SETTINGS, true);
LOG_DBG("WEA", "fetchWeather[4] WeatherClient::getWeather after valid=%d", weatherData.valid ? 1 : 0);
if (weatherData.valid) {
state = State::WEATHER_DISPLAY;
} else {
state = State::ERROR;
errorMessage = weatherData.errorMessage.empty() ? tr(STR_WEATHER_FETCH_FAILED) : weatherData.errorMessage.c_str();
}
showRefreshPopup = false;
requestUpdate();
}
void WeatherActivity::loop() {
if (state == State::WIFI_SELECTION) {
return; // Handled by WifiSelectionActivity
}
if (state == State::ERROR) {
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
// Open weather settings (useful when no location is configured)
renderer.setOrientation(GfxRenderer::Orientation::Portrait);
startActivityForResult(std::make_unique<WeatherSettingsActivity>(renderer, mappedInput),
[this](const ActivityResult&) {
renderer.setOrientation(GfxRenderer::Orientation::LandscapeClockwise);
forceRefresh = true;
loadAndDisplay();
});
} else if (mappedInput.wasReleased(MappedInputManager::Button::Left) ||
mappedInput.wasReleased(MappedInputManager::Button::Right)) {
// Retry fetch
if (WiFi.status() == WL_CONNECTED && WiFi.localIP() != IPAddress(0, 0, 0, 0)) {
state = State::FETCHING;
requestUpdate(true);
fetchWeather();
} else {
launchWifiSelection();
}
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
onGoHome();
}
return;
}
if (state == State::LOADING_CACHE || state == State::CHECK_WIFI || state == State::FETCHING) {
if (state == State::CHECK_WIFI) {
LOG_DBG("WEA", "loop CHECK_WIFI status=%d ip=%s elapsed=%lu", (int)WiFi.status(),
WiFi.localIP().toString().c_str(), (unsigned long)(millis() - wifiWaitStartedAtMs));
if (WiFi.status() == WL_CONNECTED && WiFi.localIP() != IPAddress(0, 0, 0, 0)) {
// Give lwIP/sockets a short window to settle after activity handoff.
if (wifiWaitStartedAtMs > 0 && millis() - wifiWaitStartedAtMs < 1200) {
LOG_DBG("WEA", "loop CHECK_WIFI waiting for settle window");
return;
}
LOG_DBG("WEA", "state CHECK_WIFI -> FETCHING");
state = State::FETCHING;
requestUpdate(true);
fetchWeather();
return;
}
if (wifiWaitStartedAtMs > 0 && millis() - wifiWaitStartedAtMs > 15000) {
LOG_ERR("WEA", "CHECK_WIFI timeout after %lu ms", (unsigned long)(millis() - wifiWaitStartedAtMs));
state = State::ERROR;
errorMessage = tr(STR_WIFI_CONN_FAILED);
requestUpdate();
return;
}
}
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
onGoHome();
}
return;
}
// WEATHER_DISPLAY state
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
onGoHome();
} else if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
// Open weather settings
renderer.setOrientation(GfxRenderer::Orientation::Portrait);
startActivityForResult(std::make_unique<WeatherSettingsActivity>(renderer, mappedInput),
[this](const ActivityResult&) {
// Re-apply landscape after returning from settings
renderer.setOrientation(GfxRenderer::Orientation::LandscapeClockwise);
// Refresh after settings change
forceRefresh = true;
loadAndDisplay();
});
} else if (mappedInput.wasReleased(MappedInputManager::Button::Left) ||
mappedInput.wasReleased(MappedInputManager::Button::Right)) {
// Manual refresh with popup feedback.
showRefreshPopup = true;
if (WiFi.status() == WL_CONNECTED && WiFi.localIP() != IPAddress(0, 0, 0, 0)) {
state = State::FETCHING;
requestUpdate(true);
fetchWeather();
} else {
launchWifiSelection();
}
}
}
void WeatherActivity::render(RenderLock&&) {
renderer.clearScreen();
const auto pageWidth = renderer.getScreenWidth(); // 800 in landscape
const auto pageHeight = renderer.getScreenHeight(); // 480 in landscape
if (state == State::LOADING_CACHE || (state == State::FETCHING && (!showRefreshPopup || !weatherData.valid))) {
renderer.drawCenteredText(UI_12_FONT_ID, pageHeight / 2, tr(STR_LOADING));
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();
return;
}
if (state == State::ERROR) {
renderer.drawCenteredText(UI_12_FONT_ID, pageHeight / 2 - 20, tr(STR_ERROR_MSG));
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 10, errorMessage.c_str());
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_WEATHER_SETTINGS), tr(STR_WEATHER_REFRESH), "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();
return;
}
// DISPLAY state - Landscape layout (800x480 logical)
//
// +------------------+------------------------------------+
// | Current weather | Daily forecast cards |
// | (icon, temp, | [Day1] [Day2] [Day3] ... |
// | details) | |
// | leftPanelWidth=220| right panel = contentWidth-220 |
// +------------------+------------------------------------+
// | |
// | hourly temperature + precipitation graph |
// | height = graphHeight (currently 240) |
// +--------------------------------------------------------+
// | [Back] [Settings] [Refresh] Button hints |
// +--------------------------------------------------------+
const auto& metrics = UITheme::getInstance().getMetrics();
// In LandscapeClockwise, physical bottom buttons are at the logical LEFT edge
const int contentX = metrics.buttonHintsHeight;
const int contentWidth = pageWidth - contentX;
constexpr int leftPanelWidth = 220;
constexpr int graphHeight = 240;
const int topSectionHeight = pageHeight - graphHeight;
// Draw current conditions (left panel, offset by button hints)
renderCurrentConditions(contentX, 0, leftPanelWidth, topSectionHeight);
// Draw daily forecast (right of current conditions)
renderDailyForecast(contentX + leftPanelWidth, 0, contentWidth - leftPanelWidth, topSectionHeight);
// Separator line
renderer.drawLine(contentX, topSectionHeight, pageWidth, topSectionHeight);
// Draw hourly graph
renderHourlyGraph(contentX, topSectionHeight + 1, contentWidth, graphHeight - 1);
// Button hints
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_WEATHER_SETTINGS_SHORT), tr(STR_WEATHER_REFRESH), "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
if (state == State::FETCHING && showRefreshPopup) {
GUI.drawPopup(renderer, tr(STR_LOADING_POPUP));
}
renderer.displayBuffer();
}
void WeatherActivity::renderCurrentConditions(int x, int y, int w, int h) {
const auto& cur = weatherData.current;
// Location name
int textY = y + 15;
auto locationName = WEATHER_SETTINGS.getLocationName();
if (!locationName.empty()) {
auto truncated = renderer.truncatedText(UI_10_FONT_ID, locationName.c_str(), w - 10);
renderer.drawText(UI_10_FONT_ID, x + 5, textY, truncated.c_str(), true, EpdFontFamily::BOLD);
textY += 20;
}
// Last updated time
if (weatherData.fetchedAt > 0) {
struct tm timeinfo;
time_t localTime = weatherData.fetchedAt + weatherData.utcOffsetSeconds;
gmtime_r(&localTime, &timeinfo);
char timeBuf[32];
snprintf(timeBuf, sizeof(timeBuf), "%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min);
renderer.drawText(SMALL_FONT_ID, x + 5, textY, timeBuf);
textY += 15;
}
// Weather icon (WEATHER_ICON_LARGE x WEATHER_ICON_LARGE)
auto iconType = getWeatherIconType(cur.weatherCode, cur.isDay);
const uint8_t* icon = getWeatherIconLarge(iconType);
int iconX = x + (w - WEATHER_ICON_LARGE) / 2;
drawWeatherIconWithOrientation(renderer, icon, iconX, textY + 2, WEATHER_ICON_LARGE);
textY += WEATHER_ICON_LARGE + 5;
// Temperature (large)
char tempBuf[16];
const char* unitSuffix = WEATHER_SETTINGS.getTempUnit() == WeatherTempUnit::CELSIUS ? "C" : "F";
snprintf(tempBuf, sizeof(tempBuf), "%.1f %s", cur.temperature, unitSuffix);
int tempWidth = renderer.getTextWidth(UI_12_FONT_ID, tempBuf, EpdFontFamily::BOLD);
renderer.drawText(UI_12_FONT_ID, x + (w - tempWidth) / 2, textY, tempBuf, true, EpdFontFamily::BOLD);
textY += 22;
// Feels like
snprintf(tempBuf, sizeof(tempBuf), "%.1f %s", cur.apparentTemperature, unitSuffix);
char feelsLikeBuf[48];
snprintf(feelsLikeBuf, sizeof(feelsLikeBuf), "%s: %s", tr(STR_WEATHER_FEELS_LIKE), tempBuf);
auto feelsText = renderer.truncatedText(SMALL_FONT_ID, feelsLikeBuf, w - 10);
int feelsWidth = renderer.getTextWidth(SMALL_FONT_ID, feelsText.c_str());
renderer.drawText(SMALL_FONT_ID, x + (w - feelsWidth) / 2, textY, feelsText.c_str());
textY += 16;
// Weather description
const char* desc = I18N.get(getWeatherDescriptionStrId(cur.weatherCode));
int descWidth = renderer.getTextWidth(SMALL_FONT_ID, desc);
renderer.drawText(SMALL_FONT_ID, x + (w - descWidth) / 2, textY, desc);
textY += 20;
// Details grid
const char* windDir = getWindDirectionText(cur.windDirection);
char detailBuf[64];
const auto drawCenteredDetail = [&](const char* text) {
auto truncated = renderer.truncatedText(SMALL_FONT_ID, text, w - 10);
const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, truncated.c_str());
renderer.drawText(SMALL_FONT_ID, x + (w - textWidth) / 2, textY, truncated.c_str());
};
// Wind
snprintf(detailBuf, sizeof(detailBuf), "%s: %.0f %s %s", tr(STR_WEATHER_WIND), cur.windSpeed,
WEATHER_SETTINGS.getWindUnitParam(), windDir);
drawCenteredDetail(detailBuf);
textY += 14;
// Humidity
snprintf(detailBuf, sizeof(detailBuf), "%s: %d%%", tr(STR_WEATHER_HUMIDITY), cur.humidity);
drawCenteredDetail(detailBuf);
textY += 14;
// Precipitation
if (std::fabs(cur.precipitation) < 0.0001f) {
snprintf(detailBuf, sizeof(detailBuf), "%s: --", tr(STR_WEATHER_PRECIP));
} else {
snprintf(detailBuf, sizeof(detailBuf), "%s: %.1f %s", tr(STR_WEATHER_PRECIP), cur.precipitation,
WEATHER_SETTINGS.getPrecipUnitParam());
}
drawCenteredDetail(detailBuf);
textY += 14;
// UV Index
snprintf(detailBuf, sizeof(detailBuf), "UV: %.1f", cur.uvIndex);
drawCenteredDetail(detailBuf);
// Vertical separator
renderer.drawLine(x + w, y, x + w, y + h);
}
void WeatherActivity::renderDailyForecast(int x, int y, int w, int h) {
if (weatherData.daily.empty()) return;
// Localized weekday names (indexed by tm_wday: 0=Sun..6=Sat)
const StrId dayNameIds[] = {StrId::STR_WEATHER_DAY_SUN, StrId::STR_WEATHER_DAY_MON, StrId::STR_WEATHER_DAY_TUE,
StrId::STR_WEATHER_DAY_WED, StrId::STR_WEATHER_DAY_THU, StrId::STR_WEATHER_DAY_FRI,
StrId::STR_WEATHER_DAY_SAT};
const char* unitSuffix = WEATHER_SETTINGS.getTempUnit() == WeatherTempUnit::CELSIUS ? "C" : "F";
int numDays = static_cast<int>(weatherData.daily.size());
if (numDays <= 0) return;
// Derive weekday progression from the first forecast day to avoid timezone
// conversion edge cases causing repeated day labels.
struct tm firstDayInfo;
time_t firstLocalDate = weatherData.daily[0].date + weatherData.utcOffsetSeconds;
gmtime_r(&firstLocalDate, &firstDayInfo);
const int firstWeekday = firstDayInfo.tm_wday;
// Keep all day panels evenly split (distribute remainder 1px left-to-right)
const int baseDayWidth = w / numDays;
const int extraPx = w % numDays;
int cardX = x;
for (int i = 0; i < numDays; i++) {
const auto& day = weatherData.daily[i];
int cardWidth = baseDayWidth + (i < extraPx ? 1 : 0);
int textY = y + 10;
// Day name (localized)
struct tm timeinfo;
time_t localDate = day.date + weatherData.utcOffsetSeconds;
gmtime_r(&localDate, &timeinfo);
const int weekdayIndex = (firstWeekday + i) % 7;
const char* dayName = I18N.get(dayNameIds[weekdayIndex]);
int dayNameWidth = renderer.getTextWidth(UI_10_FONT_ID, dayName, EpdFontFamily::BOLD);
renderer.drawText(UI_10_FONT_ID, cardX + (cardWidth - dayNameWidth) / 2, textY, dayName, true, EpdFontFamily::BOLD);
textY += 20;
// Date (e.g. "Apr 3") - for all days
char dateBuf[16];
const char* monthNames[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
snprintf(dateBuf, sizeof(dateBuf), "%s %d", monthNames[timeinfo.tm_mon], timeinfo.tm_mday);
int dateWidth = renderer.getTextWidth(SMALL_FONT_ID, dateBuf);
renderer.drawText(SMALL_FONT_ID, cardX + (cardWidth - dateWidth) / 2, textY, dateBuf);
textY += 18;
// Weather icon (WEATHER_ICON_LARGE for all days)
auto iconType = getWeatherIconType(day.weatherCode, true);
const uint8_t* icon = getWeatherIconLarge(iconType);
int iconX = cardX + (cardWidth - WEATHER_ICON_LARGE) / 2;
drawWeatherIconWithOrientation(renderer, icon, iconX, textY, WEATHER_ICON_LARGE);
textY += WEATHER_ICON_LARGE + 8;
// Weather description (short)
const char* desc = I18N.get(getWeatherDescriptionStrId(day.weatherCode));
auto truncDesc = renderer.truncatedText(SMALL_FONT_ID, desc, cardWidth - 8);
int descWidth = renderer.getTextWidth(SMALL_FONT_ID, truncDesc.c_str());
renderer.drawText(SMALL_FONT_ID, cardX + (cardWidth - descWidth) / 2, textY, truncDesc.c_str());
textY += 16;
// High / Low temp with unit
char tempBuf[32];
snprintf(tempBuf, sizeof(tempBuf), "%.0f / %.0f %s", day.tempMax, day.tempMin, unitSuffix);
int tempWidth = renderer.getTextWidth(SMALL_FONT_ID, tempBuf, EpdFontFamily::BOLD);
renderer.drawText(SMALL_FONT_ID, cardX + (cardWidth - tempWidth) / 2, textY, tempBuf, true, EpdFontFamily::BOLD);
textY += 16;
// Precipitation
char precipBuf[24];
if (std::fabs(day.precipSum) < 0.0001f) {
snprintf(precipBuf, sizeof(precipBuf), "%s: --", tr(STR_WEATHER_PRECIP));
} else {
snprintf(precipBuf, sizeof(precipBuf), "%s: %.1f %s", tr(STR_WEATHER_PRECIP), day.precipSum,
WEATHER_SETTINGS.getPrecipUnitParam());
}
auto truncPrecip = renderer.truncatedText(SMALL_FONT_ID, precipBuf, cardWidth - 8);
int precipWidth = renderer.getTextWidth(SMALL_FONT_ID, truncPrecip.c_str());
renderer.drawText(SMALL_FONT_ID, cardX + (cardWidth - precipWidth) / 2, textY, truncPrecip.c_str());
textY += 16;
// UV Index
char uvBuf[16];
snprintf(uvBuf, sizeof(uvBuf), "UV: %.0f", day.uvIndexMax);
int uvWidth = renderer.getTextWidth(SMALL_FONT_ID, uvBuf);
renderer.drawText(SMALL_FONT_ID, cardX + (cardWidth - uvWidth) / 2, textY, uvBuf);
// Card separator
if (i < numDays - 1) {
renderer.drawLine(cardX + cardWidth, y, cardX + cardWidth, y + h);
}
cardX += cardWidth;
}
}
void WeatherActivity::renderHourlyGraph(int x, int y, int w, int h) {
if (weatherData.hourly.empty()) return;
// Localized weekday names (indexed by tm_wday: 0=Sun..6=Sat)
const StrId dayNameIds[] = {StrId::STR_WEATHER_DAY_SUN, StrId::STR_WEATHER_DAY_MON, StrId::STR_WEATHER_DAY_TUE,
StrId::STR_WEATHER_DAY_WED, StrId::STR_WEATHER_DAY_THU, StrId::STR_WEATHER_DAY_FRI,
StrId::STR_WEATHER_DAY_SAT};
constexpr int leftMargin = 56; // Space for Y-axis labels with unit suffix
constexpr int rightMargin = 10;
constexpr int topMargin = 28; // Keep heading clear of top temperature label
constexpr int bottomMargin = 25; // Space for X-axis labels
constexpr int precipBarMaxHeight = 30;
const int graphX = x + leftMargin;
const int graphY = y + topMargin;
const int graphW = w - leftMargin - rightMargin;
const int graphH = h - topMargin - bottomMargin - precipBarMaxHeight;
const int precipY = graphY + graphH;
size_t numPoints = weatherData.hourly.size();
if (numPoints < 2) return;
// Find day boundaries in hourly data (start index of each day).
std::vector<size_t> dayStartIndices;
dayStartIndices.reserve(8);
dayStartIndices.push_back(0);
struct tm prevDayInfo;
time_t prevLocalTime = weatherData.hourly[0].time + weatherData.utcOffsetSeconds;
gmtime_r(&prevLocalTime, &prevDayInfo);
for (size_t i = 1; i < numPoints; i++) {
struct tm curDayInfo;
time_t localTime = weatherData.hourly[i].time + weatherData.utcOffsetSeconds;
gmtime_r(&localTime, &curDayInfo);
if (curDayInfo.tm_year != prevDayInfo.tm_year || curDayInfo.tm_yday != prevDayInfo.tm_yday) {
dayStartIndices.push_back(i);
prevDayInfo = curDayInfo;
}
}
// Find temperature range
float tempMin = weatherData.hourly[0].temperature;
float tempMax = tempMin;
float maxPrecip = 0;
for (const auto& hr : weatherData.hourly) {
tempMin = std::min(tempMin, hr.temperature);
tempMax = std::max(tempMax, hr.temperature);
maxPrecip = std::max(maxPrecip, hr.precipitation);
}
// Add padding to temp range
float tempRange = tempMax - tempMin;
if (tempRange < 5.0f) {
float mid = (tempMax + tempMin) / 2.0f;
tempMin = mid - 2.5f;
tempMax = mid + 2.5f;
tempRange = 5.0f;
}
tempMin -= 1.0f;
tempMax += 1.0f;
tempRange = tempMax - tempMin;
// Draw graph title centered over the graph area.
const char* graphTitle = tr(STR_WEATHER_48H_FORECAST);
const int graphTitleWidth = renderer.getTextWidth(SMALL_FONT_ID, graphTitle, EpdFontFamily::BOLD);
const int graphTitleX = graphX + (graphW - graphTitleWidth) / 2;
renderer.drawText(SMALL_FONT_ID, graphTitleX, y + 3, graphTitle, true, EpdFontFamily::BOLD);
// Draw weekday labels above the chart area at each day start.
for (size_t startIdx : dayStartIndices) {
struct tm dayInfo;
time_t localTime = weatherData.hourly[startIdx].time + weatherData.utcOffsetSeconds;
gmtime_r(&localTime, &dayInfo);
const char* dayName = I18N.get(dayNameIds[dayInfo.tm_wday]);
const int dayLabelWidth = renderer.getTextWidth(SMALL_FONT_ID, dayName, EpdFontFamily::BOLD);
const int px = graphX + static_cast<int>(startIdx * graphW / (numPoints - 1));
int labelX = px + 2;
const int maxLabelX = graphX + graphW - dayLabelWidth - 2;
if (labelX > maxLabelX) {
labelX = maxLabelX;
}
if (labelX < graphX + 2) {
labelX = graphX + 2;
}
renderer.drawText(SMALL_FONT_ID, labelX, y + 14, dayName, true, EpdFontFamily::BOLD);
}
// Draw Y-axis labels (temperature)
float tempStep = tempRange / 4.0f;
const char* unitSuffix = WEATHER_SETTINGS.getTempUnit() == WeatherTempUnit::CELSIUS ? "C" : "F";
for (int i = 0; i <= 4; i++) {
float temp = tempMax - i * tempStep;
int labelY = graphY + i * graphH / 4;
char label[16];
snprintf(label, sizeof(label), "%.0f %s", temp, unitSuffix);
renderer.drawText(SMALL_FONT_ID, x + 2, labelY - 4, label);
// Grid line (dashed effect using short segments)
for (int gx = graphX; gx < graphX + graphW; gx += 8) {
renderer.drawPixel(gx, labelY);
}
}
// Draw temperature line
int prevPx = -1, prevPy = -1;
for (size_t i = 0; i < numPoints; i++) {
int px = graphX + static_cast<int>(i * graphW / (numPoints - 1));
float normalized = (weatherData.hourly[i].temperature - tempMin) / tempRange;
int py = graphY + graphH - static_cast<int>(normalized * graphH);
if (prevPx >= 0) {
renderer.drawLine(prevPx, prevPy, px, py, 2, true);
}
prevPx = px;
prevPy = py;
}
// Draw precipitation bars
if (maxPrecip > 0) {
for (size_t i = 0; i < numPoints; i++) {
float precip = weatherData.hourly[i].precipitation;
if (precip <= 0) continue;
int px = graphX + static_cast<int>(i * graphW / (numPoints - 1));
int barH = static_cast<int>((precip / maxPrecip) * precipBarMaxHeight);
if (barH < 1) barH = 1;
int barW = std::max(1, graphW / static_cast<int>(numPoints) - 1);
renderer.fillRect(px - barW / 2, precipY + precipBarMaxHeight - barH, barW, barH);
}
}
// Draw day separator lines for day boundaries after the first day.
for (size_t boundaryIdx = 1; boundaryIdx < dayStartIndices.size(); boundaryIdx++) {
const size_t i = dayStartIndices[boundaryIdx];
const int px = graphX + static_cast<int>(i * graphW / (numPoints - 1));
renderer.drawLine(px, graphY, px, precipY + precipBarMaxHeight);
}
// Draw X-axis labels (every 6 hours)
for (size_t i = 0; i < numPoints; i += 6) {
int px = graphX + static_cast<int>(i * graphW / (numPoints - 1));
struct tm timeinfo;
time_t localTime = weatherData.hourly[i].time + weatherData.utcOffsetSeconds;
gmtime_r(&localTime, &timeinfo);
char label[8];
snprintf(label, sizeof(label), "%02d:00", timeinfo.tm_hour);
renderer.drawText(SMALL_FONT_ID, px - 12, precipY + precipBarMaxHeight + 3, label);
// Vertical grid line
for (int gy = graphY; gy < precipY + precipBarMaxHeight; gy += 8) {
renderer.drawPixel(px, gy);
}
}
// Draw axes
renderer.drawLine(graphX, graphY, graphX, precipY + precipBarMaxHeight); // Y axis
renderer.drawLine(graphX, precipY + precipBarMaxHeight, graphX + graphW,
precipY + precipBarMaxHeight); // X axis
}
+53
View File
@@ -0,0 +1,53 @@
#pragma once
#include <WeatherData.h>
#include <string>
#include "../Activity.h"
#include "util/ButtonNavigator.h"
/**
* Weather forecast display activity.
* Renders in landscape mode (800x480) with:
* - Current conditions (left panel)
* - Daily forecast cards (center)
* - 48-hour temperature + precipitation graph (bottom)
*
* On enter: loads cache -> if stale, connects WiFi -> fetches data -> displays.
* Keys: Back = return, Confirm = open settings, Left = refresh.
*/
class WeatherActivity final : public Activity {
public:
enum class State { LOADING_CACHE, CHECK_WIFI, WIFI_SELECTION, FETCHING, WEATHER_DISPLAY, ERROR };
explicit WeatherActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
: Activity("Weather", renderer, mappedInput) {}
void onEnter() override;
void onExit() override;
void loop() override;
void render(RenderLock&&) override;
private:
ButtonNavigator buttonNavigator;
State state = State::LOADING_CACHE;
WeatherData weatherData;
std::string errorMessage;
bool forceRefresh = false;
bool showRefreshPopup = false;
uint32_t wifiWaitStartedAtMs = 0;
void loadAndDisplay();
void checkAndConnectWifi();
void launchWifiSelection();
void onWifiSelectionComplete(bool connected);
void fetchWeather();
// Render sub-sections (landscape 800x480)
void renderCurrentConditions(int x, int y, int w, int h);
void renderDailyForecast(int x, int y, int w, int h);
void renderHourlyGraph(int x, int y, int w, int h);
bool preventAutoSleep() override { return true; }
};
@@ -0,0 +1,290 @@
#include "WeatherSettingsActivity.h"
#include <GfxRenderer.h>
#include <I18n.h>
#include <Logging.h>
#include <WeatherClient.h>
#include <WeatherSettingsStore.h>
#include <WiFi.h>
#include "MappedInputManager.h"
#include "activities/network/WifiSelectionActivity.h"
#include "activities/util/KeyboardEntryActivity.h"
#include "components/UITheme.h"
#include "fontIds.h"
namespace {
const StrId menuNames[] = {
StrId::STR_WEATHER_LOCATION, // 0: Search city
StrId::STR_WEATHER_LATITUDE, // 1: Manual latitude
StrId::STR_WEATHER_LONGITUDE, // 2: Manual longitude
StrId::STR_WEATHER_TEMP_UNIT, // 3: Temperature unit
StrId::STR_WEATHER_WIND_UNIT, // 4: Wind speed unit
StrId::STR_WEATHER_PRECIP_UNIT, // 5: Precipitation unit
};
} // namespace
void WeatherSettingsActivity::onEnter() {
Activity::onEnter();
selectedIndex = 0;
showingSearchResults = false;
requestUpdate();
}
void WeatherSettingsActivity::onExit() { Activity::onExit(); }
void WeatherSettingsActivity::loop() {
if (showingSearchResults) {
// Handle search results navigation
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
showingSearchResults = false;
requestUpdate();
return;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
if (!searchResults.empty() && selectedIndex < searchResults.size()) {
const auto& result = searchResults[selectedIndex];
WEATHER_SETTINGS.setLocation(result.latitude, result.longitude, result.name + ", " + result.country);
WEATHER_SETTINGS.saveToFile();
showingSearchResults = false;
selectedIndex = 0;
requestUpdate();
}
return;
}
buttonNavigator.onNext([this] {
selectedIndex = (selectedIndex + 1) % searchResults.size();
requestUpdate();
});
buttonNavigator.onPrevious([this] {
selectedIndex = (selectedIndex + searchResults.size() - 1) % searchResults.size();
requestUpdate();
});
return;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
finish();
return;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
handleSelection();
return;
}
buttonNavigator.onNext([this] {
selectedIndex = (selectedIndex + 1) % MENU_ITEMS;
requestUpdate();
});
buttonNavigator.onPrevious([this] {
selectedIndex = (selectedIndex + MENU_ITEMS - 1) % MENU_ITEMS;
requestUpdate();
});
}
void WeatherSettingsActivity::handleSelection() {
switch (selectedIndex) {
case 0:
launchCitySearch();
break;
case 1:
launchLatitudeEntry();
break;
case 2:
launchLongitudeEntry();
break;
case 3:
toggleTempUnit();
break;
case 4:
toggleWindUnit();
break;
case 5:
togglePrecipUnit();
break;
}
}
void WeatherSettingsActivity::launchCitySearch() {
startActivityForResult(
std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_WEATHER_SEARCH_CITY), "", 64, false),
[this](const ActivityResult& result) {
if (result.isCancelled) return;
const auto& kb = std::get<KeyboardResult>(result.data);
if (kb.text.empty()) return;
// Need WiFi for geocoding
if (WiFi.status() != WL_CONNECTED || WiFi.localIP() == IPAddress(0, 0, 0, 0)) {
startActivityForResult(std::make_unique<WifiSelectionActivity>(renderer, mappedInput),
[this, query = kb.text](const ActivityResult& wifiResult) {
if (wifiResult.isCancelled) return;
searchResults = WeatherClient::searchCity(query);
if (searchResults.empty()) {
requestUpdate();
return;
}
showingSearchResults = true;
selectedIndex = 0;
requestUpdate();
});
} else {
searchResults = WeatherClient::searchCity(kb.text);
if (searchResults.empty()) {
requestUpdate();
return;
}
showingSearchResults = true;
selectedIndex = 0;
requestUpdate();
}
});
}
void WeatherSettingsActivity::launchLatitudeEntry() {
std::string current = std::to_string(WEATHER_SETTINGS.getLatitude());
startActivityForResult(
std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_WEATHER_LATITUDE), current, 12, false),
[this](const ActivityResult& result) {
if (!result.isCancelled) {
const auto& kb = std::get<KeyboardResult>(result.data);
float lat = strtof(kb.text.c_str(), nullptr);
if (lat >= -90.0f && lat <= 90.0f) {
WEATHER_SETTINGS.setLocation(lat, WEATHER_SETTINGS.getLongitude(), WEATHER_SETTINGS.getLocationName());
WEATHER_SETTINGS.saveToFile();
}
}
});
}
void WeatherSettingsActivity::launchLongitudeEntry() {
std::string current = std::to_string(WEATHER_SETTINGS.getLongitude());
startActivityForResult(
std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_WEATHER_LONGITUDE), current, 12, false),
[this](const ActivityResult& result) {
if (!result.isCancelled) {
const auto& kb = std::get<KeyboardResult>(result.data);
float lon = strtof(kb.text.c_str(), nullptr);
if (lon >= -180.0f && lon <= 180.0f) {
WEATHER_SETTINGS.setLocation(WEATHER_SETTINGS.getLatitude(), lon, WEATHER_SETTINGS.getLocationName());
WEATHER_SETTINGS.saveToFile();
}
}
});
}
void WeatherSettingsActivity::toggleTempUnit() {
auto current = WEATHER_SETTINGS.getTempUnit();
WEATHER_SETTINGS.setTempUnit(current == WeatherTempUnit::CELSIUS ? WeatherTempUnit::FAHRENHEIT
: WeatherTempUnit::CELSIUS);
WEATHER_SETTINGS.saveToFile();
requestUpdate();
}
void WeatherSettingsActivity::toggleWindUnit() {
auto current = static_cast<uint8_t>(WEATHER_SETTINGS.getWindUnit());
WEATHER_SETTINGS.setWindUnit(static_cast<WeatherWindUnit>((current + 1) % 4));
WEATHER_SETTINGS.saveToFile();
requestUpdate();
}
void WeatherSettingsActivity::togglePrecipUnit() {
auto current = WEATHER_SETTINGS.getPrecipUnit();
WEATHER_SETTINGS.setPrecipUnit(current == WeatherPrecipUnit::MM ? WeatherPrecipUnit::INCH : WeatherPrecipUnit::MM);
WEATHER_SETTINGS.saveToFile();
requestUpdate();
}
void WeatherSettingsActivity::render(RenderLock&&) {
renderer.clearScreen();
const auto& metrics = UITheme::getInstance().getMetrics();
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
if (showingSearchResults) {
// Display search results
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight},
tr(STR_WEATHER_SEARCH_RESULTS));
const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing;
const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing * 2;
if (searchResults.empty()) {
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, tr(STR_NO_ENTRIES));
} else {
GUI.drawList(
renderer, Rect{0, contentTop, pageWidth, contentHeight}, static_cast<int>(searchResults.size()),
static_cast<int>(selectedIndex),
[this](int index) {
const auto& r = searchResults[index];
std::string label = r.name;
if (!r.admin1.empty()) label += ", " + r.admin1;
if (!r.country.empty()) label += ", " + r.country;
return label;
},
nullptr, nullptr, nullptr, true);
}
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();
return;
}
// Main settings menu
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_WEATHER_SETTINGS));
const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing;
const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing * 2;
GUI.drawList(
renderer, Rect{0, contentTop, pageWidth, contentHeight}, MENU_ITEMS, static_cast<int>(selectedIndex),
[](int index) { return std::string(I18N.get(menuNames[index])); }, nullptr, nullptr,
[this](int index) -> std::string {
switch (index) {
case 0: {
auto name = WEATHER_SETTINGS.getLocationName();
return name.empty() ? std::string(tr(STR_NOT_SET)) : name;
}
case 1: {
char buf[16];
snprintf(buf, sizeof(buf), "%.4f", WEATHER_SETTINGS.getLatitude());
return std::string(buf);
}
case 2: {
char buf[16];
snprintf(buf, sizeof(buf), "%.4f", WEATHER_SETTINGS.getLongitude());
return std::string(buf);
}
case 3:
return WEATHER_SETTINGS.getTempUnit() == WeatherTempUnit::CELSIUS ? "C" : "F";
case 4:
switch (WEATHER_SETTINGS.getWindUnit()) {
case WeatherWindUnit::KMH:
return "km/h";
case WeatherWindUnit::MS:
return "m/s";
case WeatherWindUnit::MPH:
return "mph";
case WeatherWindUnit::KNOTS:
return "kn";
}
return "km/h";
case 5:
return WEATHER_SETTINGS.getPrecipUnit() == WeatherPrecipUnit::MM ? "mm" : "in";
}
return "";
},
true);
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();
}
@@ -0,0 +1,44 @@
#pragma once
#include <WeatherData.h>
#include <string>
#include <vector>
#include "../Activity.h"
#include "util/ButtonNavigator.h"
/**
* Settings submenu for weather configuration.
* Supports city search via geocoding, manual lat/lon entry, and unit selection.
*/
class WeatherSettingsActivity final : public Activity {
public:
explicit WeatherSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
: Activity("WeatherSettings", renderer, mappedInput) {}
void onEnter() override;
void onExit() override;
void loop() override;
void render(RenderLock&&) override;
private:
ButtonNavigator buttonNavigator;
size_t selectedIndex = 0;
// City search results (populated when user searches)
std::vector<GeocodingResult> searchResults;
bool showingSearchResults = false;
static constexpr int MENU_ITEMS = 6; // Location, Lat, Lon, TempUnit, WindUnit, PrecipUnit
void handleSelection();
void launchCitySearch();
void launchLatitudeEntry();
void launchLongitudeEntry();
void toggleTempUnit();
void toggleWindUnit();
void togglePrecipUnit();
bool preventAutoSleep() override { return true; }
};
+2
View File
@@ -21,6 +21,7 @@
#include "KOReaderCredentialStore.h"
#include "MappedInputManager.h"
#include "RecentBooksStore.h"
#include "WeatherSettingsStore.h"
#include "activities/Activity.h"
#include "activities/ActivityManager.h"
#include "components/UITheme.h"
@@ -264,6 +265,7 @@ void setup() {
HalClock::applyTimezone(SETTINGS.timeZone);
I18N.loadSettings();
KOREADER_STORE.loadFromFile();
WEATHER_SETTINGS.loadFromFile();
UITheme::getInstance().reload();
ButtonNavigator::setMappedInputManager(mappedInputManager);
+30
View File
@@ -55,37 +55,52 @@ class FileWriteStream final : public Stream {
bool HttpDownloader::fetchUrl(const std::string& url, Stream& outContent) {
// Use NetworkClientSecure for HTTPS, regular NetworkClient for HTTP
std::unique_ptr<NetworkClient> client;
LOG_DBG("HTTP", "fetchUrl[1] start (https=%d)", UrlUtils::isHttpsUrl(url) ? 1 : 0);
if (UrlUtils::isHttpsUrl(url)) {
auto* secureClient = new NetworkClientSecure();
LOG_DBG("HTTP", "fetchUrl[2] created NetworkClientSecure");
secureClient->setInsecure();
LOG_DBG("HTTP", "fetchUrl[3] setInsecure done");
client.reset(secureClient);
} else {
client.reset(new NetworkClient());
LOG_DBG("HTTP", "fetchUrl[2] created NetworkClient");
}
HTTPClient http;
LOG_DBG("HTTP", "Fetching: %s", url.c_str());
LOG_DBG("HTTP", "fetchUrl[4] begin() before");
http.begin(*client, url.c_str());
LOG_DBG("HTTP", "fetchUrl[5] begin() after");
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
LOG_DBG("HTTP", "fetchUrl[6] redirects configured");
http.addHeader("User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION);
LOG_DBG("HTTP", "fetchUrl[7] user-agent header added");
// Add Basic HTTP auth if credentials are configured
if (strlen(SETTINGS.opdsUsername) > 0 && strlen(SETTINGS.opdsPassword) > 0) {
std::string credentials = std::string(SETTINGS.opdsUsername) + ":" + SETTINGS.opdsPassword;
String encoded = base64::encode(credentials.c_str());
http.addHeader("Authorization", "Basic " + encoded);
LOG_DBG("HTTP", "fetchUrl[8] auth header added");
}
LOG_DBG("HTTP", "fetchUrl[9] GET() before");
const int httpCode = http.GET();
LOG_DBG("HTTP", "fetchUrl[10] GET() after code=%d", httpCode);
if (httpCode != HTTP_CODE_OK) {
LOG_ERR("HTTP", "Fetch failed: %d", httpCode);
LOG_DBG("HTTP", "fetchUrl[11] end() after GET failure");
http.end();
return false;
}
LOG_DBG("HTTP", "fetchUrl[12] writeToStream() before");
http.writeToStream(&outContent);
LOG_DBG("HTTP", "fetchUrl[13] writeToStream() after");
LOG_DBG("HTTP", "fetchUrl[14] end() before success return");
http.end();
LOG_DBG("HTTP", "Fetch success");
@@ -105,30 +120,41 @@ HttpDownloader::DownloadError HttpDownloader::downloadToFile(const std::string&
ProgressCallback progress) {
// Use NetworkClientSecure for HTTPS, regular NetworkClient for HTTP
std::unique_ptr<NetworkClient> client;
LOG_DBG("HTTP", "downloadToFile[1] start (https=%d)", UrlUtils::isHttpsUrl(url) ? 1 : 0);
if (UrlUtils::isHttpsUrl(url)) {
auto* secureClient = new NetworkClientSecure();
LOG_DBG("HTTP", "downloadToFile[2] created NetworkClientSecure");
secureClient->setInsecure();
LOG_DBG("HTTP", "downloadToFile[3] setInsecure done");
client.reset(secureClient);
} else {
client.reset(new NetworkClient());
LOG_DBG("HTTP", "downloadToFile[2] created NetworkClient");
}
HTTPClient http;
LOG_DBG("HTTP", "Downloading: %s", url.c_str());
LOG_DBG("HTTP", "Destination: %s", destPath.c_str());
LOG_DBG("HTTP", "downloadToFile[4] begin() before");
http.begin(*client, url.c_str());
LOG_DBG("HTTP", "downloadToFile[5] begin() after");
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
LOG_DBG("HTTP", "downloadToFile[6] redirects configured");
http.addHeader("User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION);
LOG_DBG("HTTP", "downloadToFile[7] user-agent header added");
// Add Basic HTTP auth if credentials are configured
if (strlen(SETTINGS.opdsUsername) > 0 && strlen(SETTINGS.opdsPassword) > 0) {
std::string credentials = std::string(SETTINGS.opdsUsername) + ":" + SETTINGS.opdsPassword;
String encoded = base64::encode(credentials.c_str());
http.addHeader("Authorization", "Basic " + encoded);
LOG_DBG("HTTP", "downloadToFile[8] auth header added");
}
LOG_DBG("HTTP", "downloadToFile[9] GET() before");
const int httpCode = http.GET();
LOG_DBG("HTTP", "downloadToFile[10] GET() after code=%d", httpCode);
if (httpCode != HTTP_CODE_OK) {
LOG_ERR("HTTP", "Download failed: %d", httpCode);
http.end();
@@ -158,10 +184,14 @@ HttpDownloader::DownloadError HttpDownloader::downloadToFile(const std::string&
// Let HTTPClient handle chunked decoding and stream body bytes into the file.
FileWriteStream fileStream(file, contentLength, progress);
LOG_DBG("HTTP", "downloadToFile[11] writeToStream() before");
const int writeResult = http.writeToStream(&fileStream);
LOG_DBG("HTTP", "downloadToFile[12] writeToStream() after result=%d", writeResult);
file.close();
LOG_DBG("HTTP", "downloadToFile[13] file closed");
http.end();
LOG_DBG("HTTP", "downloadToFile[14] http end done");
if (writeResult < 0) {
LOG_ERR("HTTP", "writeToStream error: %d", writeResult);