Merge branch 'feat-weather' of https://github.com/jpirnay/crosspoint-reader into mybuild
@@ -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).
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 +=
|
||||
"¤t=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;
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(); }
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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; }
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||