├── __gmsl
├── display.jpg
├── fonts
└── ttf
│ ├── Roboto-Bold.ttf
│ └── Roboto-Regular.ttf
├── .gitignore
├── params.h
├── gfxfont.h
├── icons
└── svgs
│ ├── clear-night.svg
│ ├── partly-cloudy-night.svg
│ ├── cloudy.svg
│ ├── fog.svg
│ ├── wind.svg
│ ├── rain.svg
│ ├── clear-day.svg
│ ├── snow.svg
│ ├── partly-cloudy-day.svg
│ └── sleet.svg
├── README.md
├── Makefile
├── gmsl
├── fontconvert.c
├── LICENSE
└── eink_weather.ino
/__gmsl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jgrahamc/eink_weather/HEAD/__gmsl
--------------------------------------------------------------------------------
/display.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jgrahamc/eink_weather/HEAD/display.jpg
--------------------------------------------------------------------------------
/fonts/ttf/Roboto-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jgrahamc/eink_weather/HEAD/fonts/ttf/Roboto-Bold.ttf
--------------------------------------------------------------------------------
/fonts/ttf/Roboto-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jgrahamc/eink_weather/HEAD/fonts/ttf/Roboto-Regular.ttf
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | fontconvert
3 | fonts/gfx
4 | icons/pngs
5 | params.h
6 |
7 | # Prerequisites
8 | *.d
9 |
10 | # Compiled Object files
11 | *.slo
12 | *.lo
13 | *.o
14 | *.obj
15 |
16 | # Precompiled Headers
17 | *.gch
18 | *.pch
19 |
20 | # Compiled Dynamic libraries
21 | *.so
22 | *.dylib
23 | *.dll
24 |
25 | # Fortran module files
26 | *.mod
27 | *.smod
28 |
29 | # Compiled Static libraries
30 | *.lai
31 | *.la
32 | *.a
33 | *.lib
34 |
35 | # Executables
36 | *.exe
37 | *.out
38 | *.app
39 |
--------------------------------------------------------------------------------
/params.h:
--------------------------------------------------------------------------------
1 | // WiFi networks (SSID (network name) and password)
2 |
3 | const int wifi_count = 1;
4 | const char *wifi_networks[1] = {"TODO"};
5 | const char *wifi_passwords[1] = {"TODO"};
6 |
7 | // When to take up to check the weather forecast. This is an
8 | // array of the minutes within an hour when the display should
9 | // reload and update. For example, {0, 15, 30, 45} would update
10 | // every 15 minutes starting at the top of the hour. {0, 30}
11 | // would update at hh:00 and hh:30. These need to be in ascending
12 | // order.
13 |
14 | #define UPDATE_TIMES 2
15 | uint8_t update_times[UPDATE_TIMES] = {0, 30};
16 |
17 | // Pirate Weather API URL for a forecast. Here's where
18 | // you put your API key, the lat/long you want weather
19 | // for.
20 |
21 | const char *api_key = "TODO";
22 | const char *lat = "TODO";
23 | const char *lon = "TODO";
24 |
25 | // The title to show at the top of the display
26 |
27 | const char *title = "TODO";
28 |
29 | // Units for display, possible values: ca, uk, us, si.
30 |
31 | const char *units = "si";
32 |
--------------------------------------------------------------------------------
/gfxfont.h:
--------------------------------------------------------------------------------
1 | // Font structures for newer Adafruit_GFX (1.1 and later).
2 | // Example fonts are included in 'Fonts' directory.
3 | // To use a font in your Arduino sketch, #include the corresponding .h
4 | // file and pass address of GFXfont struct to setFont(). Pass NULL to
5 | // revert to 'classic' fixed-space bitmap font.
6 |
7 | #ifndef _GFXFONT_H_
8 | #define _GFXFONT_H_
9 |
10 | /// Font data stored PER GLYPH
11 | typedef struct {
12 | uint16_t bitmapOffset; ///< Pointer into GFXfont->bitmap
13 | uint8_t width; ///< Bitmap dimensions in pixels
14 | uint8_t height; ///< Bitmap dimensions in pixels
15 | uint8_t xAdvance; ///< Distance to advance cursor (x axis)
16 | int8_t xOffset; ///< X dist from cursor pos to UL corner
17 | int8_t yOffset; ///< Y dist from cursor pos to UL corner
18 | } GFXglyph;
19 |
20 | /// Data stored for FONT AS A WHOLE
21 | typedef struct {
22 | uint8_t *bitmap; ///< Glyph bitmaps, concatenated
23 | GFXglyph *glyph; ///< Glyph array
24 | uint16_t first; ///< ASCII extents (first char)
25 | uint16_t last; ///< ASCII extents (last char)
26 | uint8_t yAdvance; ///< Newline distance (y axis)
27 | } GFXfont;
28 |
29 | #endif // _GFXFONT_H_
30 |
--------------------------------------------------------------------------------
/icons/svgs/clear-night.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
--------------------------------------------------------------------------------
/icons/svgs/partly-cloudy-night.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # eink-weather
2 |
3 | An e-ink weather display based on the [Inkplate
4 | 10](https://soldered.com/product/soldered-inkplate-10-9-7-e-paper-board-with-enclosure-copy/)
5 | (which takes a recycled Kindle display and pairs it with an ESP32,
6 | LiPo charger, SD card, RTC) and the Pirate Weather API.
7 |
8 | 
9 |
10 | # Build
11 |
12 | 1. Get an API key from [Pirate Weather](https://pirateweather.net/).
13 |
14 | 2. Modify params.h with the WiFi network details, latitude and
15 | longitude of the location you are getting weather for, the API
16 | key and set the title to be shown on the display.
17 |
18 | 3. Run make. This will build the PNG versions of the SVG icons. If
19 | you want to change/add icons the originals come from Erik Flowers'
20 | [Weather Icons](https://erikflowers.github.io/weather-icons/).
21 |
22 | Make will also build the fonts needed by the program. These are
23 | based on Google's [Roboto](https://fonts.google.com/specimen/Roboto). You
24 | can change the fonts by changing the ttf files in fonts/ttf and
25 | then run make. You'll need to update the .ino that references the
26 | generated .h files.
27 |
28 | 4. Copy the PNG files onto the SD card to be inserted in the Inkplate.
29 | There's a make target called "copy" which will do this but you
30 | may have to modify the Makefile to set the correct destination
31 | folder.
32 |
33 | 5. Build the .ino with the Arduino IDE and upload it to the Inkplate.
34 |
35 | 6. Run it either connected via USB-C or a LiPo battery. Since it sleeps
36 | drawing microamps the battery should last months between charges.
--------------------------------------------------------------------------------
/icons/svgs/cloudy.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
19 |
--------------------------------------------------------------------------------
/icons/svgs/fog.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
19 |
--------------------------------------------------------------------------------
/icons/svgs/wind.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
22 |
--------------------------------------------------------------------------------
/icons/svgs/rain.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
24 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2023 John Graham-Cumming
2 |
3 | include gmsl
4 |
5 | .PHONY: all
6 | all:
7 |
8 | # Used to build the PNG versions of SVG weather icons in the sizes
9 | # needed by the eink-weather project. The original icons come from
10 | #
11 | # https://erikflowers.github.io/weather-icons/
12 | #
13 | # List of icon widths (in pixels) that are to be created. For each
14 | # .svg file .png versions are created with the width appended. For
15 | # example, rain.svg could become rain-24.png, rain-128.png etc.
16 | #
17 |
18 | WIDTHS := 43 128
19 |
20 | ICONS_DIR := icons/
21 | SVGS_DIR := $(ICONS_DIR)svgs/
22 | PNGS_DIR := $(ICONS_DIR)pngs/
23 |
24 | $(shell mkdir -p $(PNGS_DIR))
25 |
26 | SVGS := $(wildcard $(SVGS_DIR)*.svg)
27 |
28 | png-name = $(PNGS_DIR)$(subst $(SVGS_DIR),,$(subst .svg,,$1))-$2.png
29 | make-svg-to-png-rule = $(eval $(call png-name,$1,$2): $1 ; @rsvg-convert -d 150 -w $2 $$< -o $$@)
30 |
31 | PNGS := $(foreach w,$(WIDTHS), \
32 | $(foreach s,$(SVGS), \
33 | $(call png-name,$s,$w) \
34 | $(call make-svg-to-png-rule,$s,$w)))
35 |
36 | all: $(PNGS)
37 |
38 | # copy copies the .png files to the SD card for insertion in the
39 | # Inkplate
40 |
41 | SDCARD := /Volumes/WEATHER
42 |
43 | .PHONY: copy
44 | copy: all ; @cp $(PNGS_DIR)* $(SDCARD)
45 |
46 |
47 | # This builds the Adafruit fontconvert executable needed to convert
48 | # fonts to the GFX format
49 |
50 | CC := gcc
51 | CFLAGS := -Wall -I/usr/local/include/freetype2 -I/usr/include/freetype2 -I/usr/include
52 | LIBS := -lfreetype
53 |
54 | fontconvert: fontconvert.c gfxfont.h
55 | @$(CC) $(CFLAGS) $< $(LIBS) -o $@
56 | @strip $@
57 |
58 | # This section converts the Roboto fonts from TTF to GFX format needed
59 | # by the project. Note that we specify an extended range of glyphs
60 | # because the default does not include the degree symbol.
61 |
62 | FONTS_DIR := fonts/
63 | GFX_DIR := $(FONTS_DIR)gfx/
64 | TTF_DIR := $(FONTS_DIR)ttf/
65 |
66 | $(shell mkdir -p $(GFX_DIR))
67 |
68 | FONTS := Roboto_Bold_12 Roboto_Regular_7 Roboto_Regular_24
69 |
70 | H_FILES := $(addprefix $(GFX_DIR),$(addsuffix .h,$(FONTS)))
71 |
72 | font-name = $(subst $(GFX_DIR),$(TTF_DIR),$(word 1,$(call split,_,$(subst .h,,$1)))-$(word 2,$(call split,_,$(subst .h,,$1)))).ttf
73 | font-size = $(lastword $(call split,_,$(subst .h,,$1)))
74 | $(foreach f,$(H_FILES),$(eval $f: $(call font-name,$f) ; @./fontconvert $$< $(call font-size,$f) 32 255 > $$@))
75 |
76 | $(H_FILES): fontconvert
77 | all: $(H_FILES)
78 |
79 | # clean deletes all created files
80 |
81 | .PHONY: clean
82 | clean: ; @rm -f $(PNGS) fontconvert $(H_FILES)
83 |
84 |
--------------------------------------------------------------------------------
/icons/svgs/clear-day.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
28 |
--------------------------------------------------------------------------------
/icons/svgs/snow.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
28 |
--------------------------------------------------------------------------------
/icons/svgs/partly-cloudy-day.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
30 |
--------------------------------------------------------------------------------
/gmsl:
--------------------------------------------------------------------------------
1 | # ----------------------------------------------------------------------------
2 | #
3 | # GNU Make Standard Library (GMSL)
4 | #
5 | # A library of functions to be used with GNU Make's $(call) that
6 | # provides functionality not available in standard GNU Make.
7 | #
8 | # Copyright (c) 2005-2022 John Graham-Cumming
9 | #
10 | # This file is part of GMSL
11 | #
12 | # Redistribution and use in source and binary forms, with or without
13 | # modification, are permitted provided that the following conditions
14 | # are met:
15 | #
16 | # Redistributions of source code must retain the above copyright
17 | # notice, this list of conditions and the following disclaimer.
18 | #
19 | # Redistributions in binary form must reproduce the above copyright
20 | # notice, this list of conditions and the following disclaimer in the
21 | # documentation and/or other materials provided with the distribution.
22 | #
23 | # Neither the name of the John Graham-Cumming nor the names of its
24 | # contributors may be used to endorse or promote products derived from
25 | # this software without specific prior written permission.
26 | #
27 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
28 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
29 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
30 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
31 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
32 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
33 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
34 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
35 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
36 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
37 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
38 | # POSSIBILITY OF SUCH DAMAGE.
39 | #
40 | # ----------------------------------------------------------------------------
41 |
42 | # Determine if the library has already been included and if so don't
43 | # bother including it again
44 |
45 | ifndef __gmsl_included
46 |
47 | # Standard definitions for true and false. true is any non-empty
48 | # string, false is an empty string. These are intended for use with
49 | # $(if).
50 |
51 | true := T
52 | false :=
53 |
54 | # ----------------------------------------------------------------------------
55 | # Function: not
56 | # Arguments: 1: A boolean value
57 | # Returns: Returns the opposite of the arg. (true -> false, false -> true)
58 | # ----------------------------------------------------------------------------
59 | not = $(if $1,$(false),$(true))
60 |
61 | # Prevent reinclusion of the library
62 |
63 | __gmsl_included := $(true)
64 |
65 | # Try to determine where this file is located. If the caller did
66 | # include /foo/gmsl then extract the /foo/ so that __gmsl gets
67 | # included transparently
68 |
69 | __gmsl_root :=
70 |
71 | ifneq ($(MAKEFILE_LIST),)
72 | __gmsl_root := $(word $(words $(MAKEFILE_LIST)),$(MAKEFILE_LIST))
73 |
74 | # If there are any spaces in the path in __gmsl_root then give up
75 |
76 | ifeq (1,$(words $(__gmsl_root)))
77 | __gmsl_root := $(patsubst %gmsl,%,$(__gmsl_root))
78 | endif
79 |
80 | endif
81 |
82 | include $(__gmsl_root)__gmsl
83 |
84 | endif # __gmsl_included
85 |
86 |
--------------------------------------------------------------------------------
/icons/svgs/sleet.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
36 |
--------------------------------------------------------------------------------
/fontconvert.c:
--------------------------------------------------------------------------------
1 | /*
2 | TrueType to Adafruit_GFX font converter. Derived from Peter Jakobs'
3 | Adafruit_ftGFX fork & makefont tool, and Paul Kourany's Adafruit_mfGFX.
4 |
5 | NOT AN ARDUINO SKETCH. This is a command-line tool for preprocessing
6 | fonts to be used with the Adafruit_GFX Arduino library.
7 |
8 | For UNIX-like systems. Outputs to stdout; redirect to header file, e.g.:
9 | ./fontconvert ~/Library/Fonts/FreeSans.ttf 18 > FreeSans18pt7b.h
10 |
11 | REQUIRES FREETYPE LIBRARY. www.freetype.org
12 |
13 | Currently this only extracts the printable 7-bit ASCII chars of a font.
14 | Will eventually extend with some int'l chars a la ftGFX, not there yet.
15 | Keep 7-bit fonts around as an option in that case, more compact.
16 |
17 | See notes at end for glyph nomenclature & other tidbits.
18 | */
19 | #ifndef ARDUINO
20 |
21 | #include
22 | #include
23 | #include
24 | #include
25 | #include FT_GLYPH_H
26 | #include FT_MODULE_H
27 | #include FT_TRUETYPE_DRIVER_H
28 | #include "gfxfont.h" // Adafruit_GFX font structures
29 |
30 | #define DPI 141 // Approximate res. of Adafruit 2.8" TFT
31 |
32 | // Accumulate bits for output, with periodic hexadecimal byte write
33 | void enbit(uint8_t value) {
34 | static uint8_t row = 0, sum = 0, bit = 0x80, firstCall = 1;
35 | if (value)
36 | sum |= bit; // Set bit if needed
37 | if (!(bit >>= 1)) { // Advance to next bit, end of byte reached?
38 | if (!firstCall) { // Format output table nicely
39 | if (++row >= 12) { // Last entry on line?
40 | printf(",\n "); // Newline format output
41 | row = 0; // Reset row counter
42 | } else { // Not end of line
43 | printf(", "); // Simple comma delim
44 | }
45 | }
46 | printf("0x%02X", sum); // Write byte value
47 | sum = 0; // Clear for next byte
48 | bit = 0x80; // Reset bit counter
49 | firstCall = 0; // Formatting flag
50 | }
51 | }
52 |
53 | int main(int argc, char *argv[]) {
54 | int i, j, err, size, first = ' ', last = '~', bitmapOffset = 0, x, y, byte;
55 | char *fontName, c, *ptr;
56 | FT_Library library;
57 | FT_Face face;
58 | FT_Glyph glyph;
59 | FT_Bitmap *bitmap;
60 | FT_BitmapGlyphRec *g;
61 | GFXglyph *table;
62 | uint8_t bit;
63 |
64 | // Parse command line. Valid syntaxes are:
65 | // fontconvert [filename] [size]
66 | // fontconvert [filename] [size] [last char]
67 | // fontconvert [filename] [size] [first char] [last char]
68 | // Unless overridden, default first and last chars are
69 | // ' ' (space) and '~', respectively
70 |
71 | if (argc < 3) {
72 | fprintf(stderr, "Usage: %s fontfile size [first] [last]\n", argv[0]);
73 | return 1;
74 | }
75 |
76 | size = atoi(argv[2]);
77 |
78 | if (argc == 4) {
79 | last = atoi(argv[3]);
80 | } else if (argc == 5) {
81 | first = atoi(argv[3]);
82 | last = atoi(argv[4]);
83 | }
84 |
85 | if (last < first) {
86 | i = first;
87 | first = last;
88 | last = i;
89 | }
90 |
91 | ptr = strrchr(argv[1], '/'); // Find last slash in filename
92 | if (ptr)
93 | ptr++; // First character of filename (path stripped)
94 | else
95 | ptr = argv[1]; // No path; font in local dir.
96 |
97 | // Allocate space for font name and glyph table
98 | if ((!(fontName = malloc(strlen(ptr) + 20))) ||
99 | (!(table = (GFXglyph *)malloc((last - first + 1) * sizeof(GFXglyph))))) {
100 | fprintf(stderr, "Malloc error\n");
101 | return 1;
102 | }
103 |
104 | // Derive font table names from filename. Period (filename
105 | // extension) is truncated and replaced with the font size & bits.
106 | strcpy(fontName, ptr);
107 | ptr = strrchr(fontName, '.'); // Find last period (file ext)
108 | if (!ptr)
109 | ptr = &fontName[strlen(fontName)]; // If none, append
110 | // Insert font size and 7/8 bit. fontName was alloc'd w/extra
111 | // space to allow this, we're not sprintfing into Forbidden Zone.
112 | sprintf(ptr, "%dpt%db", size, (last > 127) ? 8 : 7);
113 | // Space and punctuation chars in name replaced w/ underscores.
114 | for (i = 0; (c = fontName[i]); i++) {
115 | if (isspace(c) || ispunct(c))
116 | fontName[i] = '_';
117 | }
118 |
119 | // Init FreeType lib, load font
120 | if ((err = FT_Init_FreeType(&library))) {
121 | fprintf(stderr, "FreeType init error: %d", err);
122 | return err;
123 | }
124 |
125 | // Use TrueType engine version 35, without subpixel rendering.
126 | // This improves clarity of fonts since this library does not
127 | // support rendering multiple levels of gray in a glyph.
128 | // See https://github.com/adafruit/Adafruit-GFX-Library/issues/103
129 | FT_UInt interpreter_version = TT_INTERPRETER_VERSION_35;
130 | FT_Property_Set(library, "truetype", "interpreter-version",
131 | &interpreter_version);
132 |
133 | if ((err = FT_New_Face(library, argv[1], 0, &face))) {
134 | fprintf(stderr, "Font load error: %d", err);
135 | FT_Done_FreeType(library);
136 | return err;
137 | }
138 |
139 | // << 6 because '26dot6' fixed-point format
140 | FT_Set_Char_Size(face, size << 6, 0, DPI, 0);
141 |
142 | // Currently all symbols from 'first' to 'last' are processed.
143 | // Fonts may contain WAY more glyphs than that, but this code
144 | // will need to handle encoding stuff to deal with extracting
145 | // the right symbols, and that's not done yet.
146 | // fprintf(stderr, "%ld glyphs\n", face->num_glyphs);
147 |
148 | printf("const uint8_t %sBitmaps[] PROGMEM = {\n ", fontName);
149 |
150 | // Process glyphs and output huge bitmap data array
151 | for (i = first, j = 0; i <= last; i++, j++) {
152 | // MONO renderer provides clean image with perfect crop
153 | // (no wasted pixels) via bitmap struct.
154 | if ((err = FT_Load_Char(face, i, FT_LOAD_TARGET_MONO))) {
155 | fprintf(stderr, "Error %d loading char '%c'\n", err, i);
156 | continue;
157 | }
158 |
159 | if ((err = FT_Render_Glyph(face->glyph, FT_RENDER_MODE_MONO))) {
160 | fprintf(stderr, "Error %d rendering char '%c'\n", err, i);
161 | continue;
162 | }
163 |
164 | if ((err = FT_Get_Glyph(face->glyph, &glyph))) {
165 | fprintf(stderr, "Error %d getting glyph '%c'\n", err, i);
166 | continue;
167 | }
168 |
169 | bitmap = &face->glyph->bitmap;
170 | g = (FT_BitmapGlyphRec *)glyph;
171 |
172 | // Minimal font and per-glyph information is stored to
173 | // reduce flash space requirements. Glyph bitmaps are
174 | // fully bit-packed; no per-scanline pad, though end of
175 | // each character may be padded to next byte boundary
176 | // when needed. 16-bit offset means 64K max for bitmaps,
177 | // code currently doesn't check for overflow. (Doesn't
178 | // check that size & offsets are within bounds either for
179 | // that matter...please convert fonts responsibly.)
180 | table[j].bitmapOffset = bitmapOffset;
181 | table[j].width = bitmap->width;
182 | table[j].height = bitmap->rows;
183 | table[j].xAdvance = face->glyph->advance.x >> 6;
184 | table[j].xOffset = g->left;
185 | table[j].yOffset = 1 - g->top;
186 |
187 | for (y = 0; y < bitmap->rows; y++) {
188 | for (x = 0; x < bitmap->width; x++) {
189 | byte = x / 8;
190 | bit = 0x80 >> (x & 7);
191 | enbit(bitmap->buffer[y * bitmap->pitch + byte] & bit);
192 | }
193 | }
194 |
195 | // Pad end of char bitmap to next byte boundary if needed
196 | int n = (bitmap->width * bitmap->rows) & 7;
197 | if (n) { // Pixel count not an even multiple of 8?
198 | n = 8 - n; // # bits to next multiple
199 | while (n--)
200 | enbit(0);
201 | }
202 | bitmapOffset += (bitmap->width * bitmap->rows + 7) / 8;
203 |
204 | FT_Done_Glyph(glyph);
205 | }
206 |
207 | printf(" };\n\n"); // End bitmap array
208 |
209 | // Output glyph attributes table (one per character)
210 | printf("const GFXglyph %sGlyphs[] PROGMEM = {\n", fontName);
211 | for (i = first, j = 0; i <= last; i++, j++) {
212 | printf(" { %5d, %3d, %3d, %3d, %4d, %4d }", table[j].bitmapOffset,
213 | table[j].width, table[j].height, table[j].xAdvance, table[j].xOffset,
214 | table[j].yOffset);
215 | if (i < last) {
216 | printf(", // 0x%02X", i);
217 | if ((i >= ' ') && (i <= '~')) {
218 | printf(" '%c'", i);
219 | }
220 | putchar('\n');
221 | }
222 | }
223 | printf(" }; // 0x%02X", last);
224 | if ((last >= ' ') && (last <= '~'))
225 | printf(" '%c'", last);
226 | printf("\n\n");
227 |
228 | // Output font structure
229 | printf("const GFXfont %s PROGMEM = {\n", fontName);
230 | printf(" (uint8_t *)%sBitmaps,\n", fontName);
231 | printf(" (GFXglyph *)%sGlyphs,\n", fontName);
232 | if (face->size->metrics.height == 0) {
233 | // No face height info, assume fixed width and get from a glyph.
234 | printf(" 0x%02X, 0x%02X, %d };\n\n", first, last, table[0].height);
235 | } else {
236 | printf(" 0x%02X, 0x%02X, %ld };\n\n", first, last,
237 | face->size->metrics.height >> 6);
238 | }
239 | printf("// Approx. %d bytes\n", bitmapOffset + (last - first + 1) * 7 + 7);
240 | // Size estimate is based on AVR struct and pointer sizes;
241 | // actual size may vary.
242 |
243 | FT_Done_FreeType(library);
244 |
245 | return 0;
246 | }
247 |
248 | /* -------------------------------------------------------------------------
249 |
250 | Character metrics are slightly different from classic GFX & ftGFX.
251 | In classic GFX: cursor position is the upper-left pixel of each 5x7
252 | character; lower extent of most glyphs (except those w/descenders)
253 | is +6 pixels in Y direction.
254 | W/new GFX fonts: cursor position is on baseline, where baseline is
255 | 'inclusive' (containing the bottom-most row of pixels in most symbols,
256 | except those with descenders; ftGFX is one pixel lower).
257 |
258 | Cursor Y will be moved automatically when switching between classic
259 | and new fonts. If you switch fonts, any print() calls will continue
260 | along the same baseline.
261 |
262 | ...........#####.. -- yOffset
263 | ..........######..
264 | ..........######..
265 | .........#######..
266 | ........#########.
267 | * = Cursor pos. ........#########.
268 | .......##########.
269 | ......#####..####.
270 | ......#####..####.
271 | *.#.. .....#####...####.
272 | .#.#. ....##############
273 | #...# ...###############
274 | #...# ...###############
275 | ##### ..#####......#####
276 | #...# .#####.......#####
277 | ====== #...# ====== #*###.........#### ======= Baseline
278 | || xOffset
279 |
280 | glyph->xOffset and yOffset are pixel offsets, in GFX coordinate space
281 | (+Y is down), from the cursor position to the top-left pixel of the
282 | glyph bitmap. i.e. yOffset is typically negative, xOffset is typically
283 | zero but a few glyphs will have other values (even negative xOffsets
284 | sometimes, totally normal). glyph->xAdvance is the distance to move
285 | the cursor on the X axis after drawing the corresponding symbol.
286 |
287 | There's also some changes with regard to 'background' color and new GFX
288 | fonts (classic fonts unchanged). See Adafruit_GFX.cpp for explanation.
289 | */
290 |
291 | #endif /* !ARDUINO */
292 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/eink_weather.ino:
--------------------------------------------------------------------------------
1 | // eink_weather: this project uses an Inkplate 10 display and the
2 | // Pirate Weather API to get a weather forecast for a given latitude
3 | // and longitude and display it.
4 | //
5 | // Copyright (c) 2023 John Graham-Cumming
6 |
7 | #include
8 | #include
9 | #include
10 | #include
11 | #include
12 | #include
13 |
14 | #include "params.h"
15 |
16 | #include "fonts/gfx/Roboto_Regular_7.h"
17 | #include "fonts/gfx/Roboto_Regular_24.h"
18 | #include "fonts/gfx/Roboto_Bold_12.h"
19 |
20 | const GFXfont *fontSmall = &Roboto_Regular7pt8b;
21 | const GFXfont *fontMedium = &Roboto_Bold12pt8b;
22 | const GFXfont *fontLarge = &Roboto_Regular24pt8b;
23 |
24 | // The default time when the RTC is not set is January 1, 2066 00:00
25 |
26 | #define DEFAULT_EPOCH 3029529605
27 |
28 | Inkplate ink(INKPLATE_3BIT);
29 |
30 | // This structure and the array are used to blend the historical weather
31 | // forecast with an up to date forecast. That way the hourly data shown
32 | // for 48 hours is a mix of what was predicted at midnight today and what
33 | // is predicted now.
34 |
35 | #define ICON_SIZE 64
36 | struct hour_slot {
37 | uint32_t when;
38 | float temperature;
39 | char icon[ICON_SIZE];
40 | };
41 |
42 | struct hour_slot hours[48];
43 |
44 | #define SECONDS_PER_HOUR (60*60)
45 | #define SECONDS_PER_DAY (24*SECONDS_PER_HOUR)
46 |
47 | // setup runs the entire program and then goes to sleep.
48 | void setup() {
49 | ink.begin();
50 |
51 | // Default so that on first start up if WiFi doesn't connect then
52 | // will try again in 60 seconds.
53 |
54 | uint64_t sleep_time = 60;
55 |
56 | if (connectWiFi(wifi_count, wifi_networks, wifi_passwords)) {
57 | if (setRTC()) {
58 |
59 | // Figure out which of the update_times[] array is the next minute
60 | // past the hour at which the display should update.
61 |
62 | uint32_t now = getRtcNow();
63 | uint32_t next = now + sleep_time;
64 |
65 | if (now < DEFAULT_EPOCH) {
66 | uint32_t this_hour = now / SECONDS_PER_HOUR;
67 | this_hour *= SECONDS_PER_HOUR;
68 |
69 | for (int h = 0; h < 2; h++) {
70 | for (int i = 0; i < UPDATE_TIMES; i++) {
71 | next = this_hour + h * SECONDS_PER_HOUR + update_times[i] * 60;
72 | if (next > now) {
73 | h = 2;
74 | break;
75 | }
76 | }
77 | }
78 | }
79 |
80 | if (ink.sdCardInit() != 0) {
81 | showWeather(now, next);
82 |
83 | if (next > now) {
84 | sleep_time = next - now;
85 | }
86 | } else {
87 | fatal("SD card failed to initialize");
88 | }
89 | } else {
90 | fatal("Failed to set RTC from NTP");
91 | }
92 |
93 | disconnectWiFi();
94 | } else {
95 | fatal("Failed to connect to WiFi ");
96 | }
97 |
98 | deepSleep(sleep_time);
99 | }
100 |
101 | // loop contains nothing because the entire sketch will be woken up
102 | // in setup(), do work and then go to sleep again.
103 | void loop() {}
104 |
105 | // connectWiFi connects to the passed in WiFi network and returns true
106 | // if successful.
107 | bool connectWiFi(int count, const char **ssids, const char **passes) {
108 | return ink.connectWiFiMulti(count, ssids, passes, 23, true);
109 | }
110 |
111 | // disconnectWiFi cleans up the result of connecting via connectWiFi
112 | void disconnectWiFi() {
113 | ink.disconnect();
114 | }
115 |
116 | // setRTC sets the RTC via NTP and returns true is successful
117 | bool setRTC() {
118 |
119 | // First 0 means no offset from GMT, second 0 means no daylight savings time.
120 |
121 | configTime(0, 0, "pool.ntp.org", "time.nist.gov", "time.cloudflare.com");
122 |
123 | int tries = 5;
124 |
125 | while (tries > 0) {
126 | delay(2000);
127 | tries -= 1;
128 |
129 | uint32_t fromntp = time(NULL);
130 |
131 | // If time from NTP doesn't look like it's been set then wait
132 |
133 | if (fromntp < 1681141968) {
134 | continue;
135 | }
136 |
137 | Serial.print("Time from NTP: ");
138 | Serial.println(fromntp);
139 |
140 | ink.rtcSetEpoch(fromntp);
141 | return true;
142 | }
143 |
144 | return false;
145 | }
146 |
147 | // gtcRtcNow returns the current epoch time from the RTC
148 | uint32_t getRtcNow() {
149 | ink.rtcGetRtcData();
150 | return ink.rtcGetEpoch();
151 | }
152 |
153 | const char days[7][4] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
154 |
155 | // day converts a Unix epoch to the current day of the week taking into
156 | // account the timezone offset in hours.
157 | const char *day(uint32_t when, float offset) {
158 | when += int(offset * SECONDS_PER_HOUR);
159 | int dow = int(floor(when / SECONDS_PER_DAY) + 4) % 7;
160 | return days[dow];
161 | }
162 |
163 | #define HHMM_SIZE 6
164 |
165 | // hhmm converts a Unix epoch to hh:mm taking into account the timezone
166 | // offset in hours. out just be at least HHMM_SIZE.
167 | void hhmm(uint32_t when, float offset, char *out) {
168 | when += int(offset * SECONDS_PER_HOUR);
169 | int hh = int(floor(when / SECONDS_PER_HOUR)) % 24;
170 | int mm = int(floor(when / 60)) % 60;
171 | sprintf(out, "%02d:%02d", hh, mm);
172 | }
173 |
174 | // prHelper does the actual printing of text. It takes an (x, y) position
175 | // of the text, the text and font. Plus two parameters wm and hm which
176 | // are multipliers to apply to the width and height of the text being
177 | // printed. If the width and height are w and h then this will calculate
178 | // x + wm*w, y + hm*h.
179 | void prHelper(int16_t x, int16_t y, const char *text, const GFXfont *f,
180 | float wm = 0, float hm = 0) {
181 | int16_t x1;
182 | int16_t y1;
183 | uint16_t w;
184 | uint16_t h;
185 | ink.setFont(f);
186 | ink.setTextSize(1);
187 | ink.getTextBounds(text, 0, 0, &x1, &y1, &w, &h);
188 | ink.setCursor(x + w * wm, y + h * hm);
189 | ink.print(text);
190 | }
191 |
192 | // pr writes text at (x, y) with font f.
193 | void pr(int16_t x, int16_t y, const char *text, const GFXfont *f) {
194 | prHelper(x, y, text, f);
195 | }
196 |
197 | // centre centres a piece of text at the x, y position.
198 | void centre(int16_t x, int16_t y, const char *text, const GFXfont *f) {
199 | prHelper(x, y, text, f, -0.5);
200 | }
201 |
202 | // right right-justifies text x, y position.
203 | void right(int16_t x, int16_t y, const char *text, const GFXfont *f) {
204 | prHelper(x, y, text, f, -1);
205 | }
206 |
207 | // flushRight right-justifies text at the y position with size s.
208 | void flushRight(int16_t y, const char *text, const GFXfont *f) {
209 | prHelper(ink.width() - 20, y, text, f, -1);
210 | }
211 |
212 | #define SMALL_IMAGE 43
213 | #define LARGE_IMAGE 128
214 |
215 | // centreIcon draws an icon cenetred at the x, y position. If the
216 | // icon name is partly-cloudy-day and the size s is 43 then this
217 | // will load partly-cloudy-day-42.png.
218 | void centreIcon(int16_t x, int16_t y, const char *img, int16_t s) {
219 | char tmp[100];
220 | sprintf(tmp, "%s-%d.png", img, s);
221 |
222 | // This assumes that icon are squares
223 |
224 | ink.drawImage(tmp, x - s / 2, y - s / 2);
225 | }
226 |
227 | // drawRectangle draws a rectangle given the top left corner and
228 | // width and height. We don't use the built in ink.drawRect because
229 | // the lines are thinner than a one pixel line drawn by drawThickLine.
230 | void drawRectangle(int16_t x0, int16_t y0, int16_t w, int16_t h) {
231 | int16_t x1 = x0 + w;
232 | int16_t y1 = y0 + h;
233 | ink.drawThickLine(x0, y0, x1, y0, 0, 1);
234 | ink.drawThickLine(x0, y1, x1, y1, 0, 1);
235 | ink.drawThickLine(x0, y0, x0, y1, 0, 1);
236 | ink.drawThickLine(x1, y0, x1, y1, 0, 1);
237 | }
238 |
239 | #define TEMP_SIZE 6
240 |
241 | // roundTemp rounds a floating point temperature and write to a string
242 | // that contains the temperature. out must be at least TEMP_SIZE.
243 | void roundTemp(float t, char *out) {
244 |
245 | // Temperatures are rounded up if above 0 and down if below 0.
246 | // Example: 1.4C becomes 1C, 1.6C becomes 2C, -0.4C becomes 0C,
247 | // -1.7C becomes -2C.
248 |
249 | if (t >= 0) {
250 | t += 0.5;
251 | } else {
252 | t -= 0.5;
253 | }
254 |
255 | sprintf(out, "%d", int(t));
256 | }
257 |
258 | // CA certificate for the domain being connected to using TLS. Obtained
259 | // using the openssl s_client as follows:
260 | //
261 | // openssl s_client -showcerts -servername api.pirateweather.net \
262 | // -connect api.pirateweather.net:443
263 | //
264 | // The certificate below is the last certificate output by openssl as it
265 | // is the CA certificate of the domain above.
266 | const char *cacert = \
267 | "-----BEGIN CERTIFICATE-----\n" \
268 | "MIIEdTCCA12gAwIBAgIJAKcOSkw0grd/MA0GCSqGSIb3DQEBCwUAMGgxCzAJBgNV\n" \
269 | "BAYTAlVTMSUwIwYDVQQKExxTdGFyZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTIw\n" \
270 | "MAYDVQQLEylTdGFyZmllbGQgQ2xhc3MgMiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0\n" \
271 | "eTAeFw0wOTA5MDIwMDAwMDBaFw0zNDA2MjgxNzM5MTZaMIGYMQswCQYDVQQGEwJV\n" \
272 | "UzEQMA4GA1UECBMHQXJpem9uYTETMBEGA1UEBxMKU2NvdHRzZGFsZTElMCMGA1UE\n" \
273 | "ChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjE7MDkGA1UEAxMyU3RhcmZp\n" \
274 | "ZWxkIFNlcnZpY2VzIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IC0gRzIwggEi\n" \
275 | "MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVDDrEKvlO4vW+GZdfjohTsR8/\n" \
276 | "y8+fIBNtKTrID30892t2OGPZNmCom15cAICyL1l/9of5JUOG52kbUpqQ4XHj2C0N\n" \
277 | "Tm/2yEnZtvMaVq4rtnQU68/7JuMauh2WLmo7WJSJR1b/JaCTcFOD2oR0FMNnngRo\n" \
278 | "Ot+OQFodSk7PQ5E751bWAHDLUu57fa4657wx+UX2wmDPE1kCK4DMNEffud6QZW0C\n" \
279 | "zyyRpqbn3oUYSXxmTqM6bam17jQuug0DuDPfR+uxa40l2ZvOgdFFRjKWcIfeAg5J\n" \
280 | "Q4W2bHO7ZOphQazJ1FTfhy/HIrImzJ9ZVGif/L4qL8RVHHVAYBeFAlU5i38FAgMB\n" \
281 | "AAGjgfAwge0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0O\n" \
282 | "BBYEFJxfAN+qAdcwKziIorhtSpzyEZGDMB8GA1UdIwQYMBaAFL9ft9HO3R+G9FtV\n" \
283 | "rNzXEMIOqYjnME8GCCsGAQUFBwEBBEMwQTAcBggrBgEFBQcwAYYQaHR0cDovL28u\n" \
284 | "c3MyLnVzLzAhBggrBgEFBQcwAoYVaHR0cDovL3guc3MyLnVzL3guY2VyMCYGA1Ud\n" \
285 | "HwQfMB0wG6AZoBeGFWh0dHA6Ly9zLnNzMi51cy9yLmNybDARBgNVHSAECjAIMAYG\n" \
286 | "BFUdIAAwDQYJKoZIhvcNAQELBQADggEBACMd44pXyn3pF3lM8R5V/cxTbj5HD9/G\n" \
287 | "VfKyBDbtgB9TxF00KGu+x1X8Z+rLP3+QsjPNG1gQggL4+C/1E2DUBc7xgQjB3ad1\n" \
288 | "l08YuW3e95ORCLp+QCztweq7dp4zBncdDQh/U90bZKuCJ/Fp1U1ervShw3WnWEQt\n" \
289 | "8jxwmKy6abaVd38PMV4s/KCHOkdp8Hlf9BRUpJVeEXgSYCfOn8J3/yNTd126/+pZ\n" \
290 | "59vPr5KW7ySaNRB6nJHGDn2Z9j8Z3/VyVOEVqQdZe4O/Ui5GjLIAZHYcSNPYeehu\n" \
291 | "VsyuLAOQ1xk4meTKCRlb/weWsKh/NEnfVqn3sF/tM+2MR7cwA130A4w=\n" \
292 | "-----END CERTIFICATE-----\n";
293 |
294 | // callAPI calls the Pirate Weather API with a set of excluded sections (which
295 | // must not be empty) and an epoch time for when the weather forecast should
296 | // start. Either returns the HTTP body as a String or an empty string for an
297 | // error. If when is zero then gets the current forecast.
298 | String callAPI(char *exclude, uint32_t when) {
299 | WiFiClientSecure tls;
300 | tls.setCACert(cacert);
301 | HTTPClient http;
302 |
303 | // The API URL. A number of elements of the response can be filtered
304 | // using exclude to minimize the size of the JSON response.
305 |
306 | const char *api_format = \
307 | "https://api.pirateweather.net/forecast/" \
308 | "%s/%s,%s%s" \
309 | "?units=%s" \
310 | "&tz=precise" \
311 | "&exclude=%s";
312 |
313 | char api[232];
314 | char whens[32] = "";
315 | if (when != 0) {
316 | sprintf(whens, ",%d", when);
317 | }
318 | sprintf(api, api_format, api_key, lat, lon, whens, units, exclude);
319 |
320 | int tries = 5;
321 |
322 | // Retry API calls at most five times with two seconds between
323 | // retries.
324 |
325 | int code;
326 |
327 | while (tries > 0) {
328 | tries -= 1;
329 |
330 | if (http.begin(tls, api)) {
331 | code = http.GET();
332 | if (code == HTTP_CODE_OK) {
333 | return http.getString();
334 | }
335 | }
336 |
337 | delay(2000);
338 | }
339 |
340 | fatal("Pirate Weather API call failed " + http.errorToString(code));
341 | return "";
342 | }
343 |
344 | // showWeather gets and displays the weather forecast on screen.
345 | void showWeather(uint32_t update_time, uint32_t next) {
346 |
347 | // Step 1.
348 | //
349 | // Since the hourly section of the Pirate Weather API returns the next
350 | // 48 hours and we want today and tomorrow we need to ask for midnight
351 | // today. This is done by retrieving the time now from the RTC. But there's
352 | // a hitch: we want local time so... we first make an API call to the
353 | // Pirate Weather API excluding all sections so we can just extract the UTC
354 | // offset.
355 |
356 | uint32_t now = getRtcNow();
357 |
358 | // This means that clock hasn't been set
359 |
360 | if (now >= 3029529605) {
361 | return;
362 | }
363 |
364 | String response = callAPI("currently,minutely,hourly,daily,alerts", 0);
365 | if (response == "") {
366 | return;
367 | }
368 |
369 | // This isn't efficient because the return from http.getString() is
370 | // a String which will get duplicated by deserializeJson. Size
371 | // determined using https://arduinojson.org/v6/assistant/#/step1
372 |
373 | StaticJsonDocument<1536> jsonTiming;
374 | DeserializationError err = deserializeJson(jsonTiming, response);
375 |
376 | if (err) {
377 | fatal("Deserialize JSON failed " + String(err.c_str()));
378 | return;
379 | }
380 |
381 | // This will be the delta (in hours) between UTC and the local time. For example,
382 | // in the UK during the summer this will be 1 and to go from UTC to local time
383 | // you must add 1. Note that this is a float because the offset could be 1.5 hours
384 | // or similar (e.g. India is 5.5 hours from UTC).
385 |
386 | float offset = jsonTiming["offset"];
387 | const char *tz = jsonTiming["timezone"];
388 |
389 | // Figure out the epoch equivalent of midnight local time. First round to the nearest day.
390 | // Since the epoch starts on a midnight boundary this will give us UTC midnight for today.
391 | // The subtract the offset hours to get the UTC time that corresponds to local midnight.
392 |
393 | uint32_t seconds_since_midnight = now;
394 |
395 | // Do not be tempted to remove these two lines. This is integer division so this is used
396 | // for rounding!
397 |
398 | now /= SECONDS_PER_DAY;
399 | now *= SECONDS_PER_DAY;
400 |
401 | uint32_t midnight = now - int(offset * SECONDS_PER_HOUR);
402 | seconds_since_midnight -= midnight;
403 |
404 | // Step 2.
405 | //
406 | // Now get the historial weather forecast from the calculated local midnight and insert
407 | // it into the hours array.
408 |
409 | response = callAPI("currently,daily,minutely,alerts", midnight);
410 | if (response == "") {
411 | return;
412 | }
413 |
414 | DynamicJsonDocument doc(32768);
415 | err = deserializeJson(doc, response);
416 |
417 | if (err) {
418 | fatal("Deserialize JSON failed " + String(err.c_str()));
419 | return;
420 | }
421 |
422 | int i = 0;
423 |
424 | JsonObject hourly = doc["hourly"];
425 | for (JsonObject hourly_data_item : hourly["data"].as()) {
426 | hours[i].when = hourly_data_item["time"];
427 | hours[i].temperature = hourly_data_item["temperature"];
428 | strcpy(hours[i].icon, hourly_data_item["icon"]);
429 | i += 1;
430 | }
431 |
432 | // Step 3.
433 | //
434 | // Then get the current forecast and overwrite entries in the hours
435 | // array so we blend the historical forecast and the current one.
436 |
437 | response = callAPI("currently,minutely,alerts", 0);
438 | if (response == "") {
439 | return;
440 | }
441 |
442 | doc.clear();
443 | err = deserializeJson(doc, response);
444 |
445 | if (err) {
446 | fatal("Deserialize JSON failed " + String(err.c_str()));
447 | return;
448 | }
449 |
450 | hourly = doc["hourly"];
451 | for (JsonObject hourly_data_item : hourly["data"].as()) {
452 | for (i = 0; i < 48; i++) {
453 | if (hours[i].when == hourly_data_item["time"]) {
454 | hours[i].temperature = hourly_data_item["temperature"];
455 | strcpy(hours[i].icon, hourly_data_item["icon"]);
456 | }
457 | }
458 | }
459 |
460 | // Step 4.
461 | //
462 | // Draw the hourly data on screen and since the last API call included the data
463 | // for the next 7 days draw the daily forecast as well.
464 |
465 | int16_t bar_start_x = 50;
466 | int16_t bar_start_y = 200;
467 |
468 | // This ensure that the bar_width is a multiple of 24 so that the hours are spaced
469 | // at an integer number of pixels.
470 |
471 | int16_t bar_width = ink.width() - 2 * bar_start_x;
472 | int16_t temp_bar_width = bar_width;
473 | bar_width /= 24;
474 | int16_t bar_hour_spacing = bar_width;
475 | bar_width *= 24;
476 |
477 | bar_start_x += (temp_bar_width - bar_width) / 2;
478 |
479 | int16_t bar_height = 100;
480 | int16_t bar_gap = 100;
481 |
482 | drawRectangle(bar_start_x, bar_start_y, bar_width, bar_height);
483 | drawRectangle(bar_start_x, bar_start_y + bar_height + bar_gap, bar_width, bar_height);
484 |
485 | // This draws a marker showing the current time relative to the today weather forecast
486 |
487 | uint32_t now_offset = ((uint32_t)bar_width * seconds_since_midnight) / SECONDS_PER_DAY;
488 | int16_t now_x = bar_start_x + now_offset;
489 | ink.fillTriangle(now_x - 3, bar_start_y - 7, now_x + 3, bar_start_y - 6, now_x, bar_start_y - 1, 1);
490 |
491 | ink.setTextColor(0, 7);
492 | pr(bar_start_x, bar_start_y - 7, "Today", fontMedium);
493 | pr(bar_start_x, bar_start_y + bar_height + bar_gap - 7, "Tomorrow", fontMedium);
494 |
495 | int16_t bar_y = bar_start_y;
496 | int16_t bar_x = bar_start_x;
497 | int16_t short_tick = 5;
498 | int16_t long_tick = 10;
499 |
500 | int16_t last_x = -1;
501 | char last[128];
502 | for (i = 0; i < 48; i++) {
503 | ink.drawThickLine(bar_x, bar_y + bar_height, bar_x,
504 | bar_y + bar_height + ((i % 2 == 0) ? short_tick : long_tick), 0, 1);
505 |
506 | if ((i % 2) == 0) {
507 | char hour[HHMM_SIZE];
508 | hhmm(hours[i].when, offset, &hour[0]);
509 | char temp[TEMP_SIZE];
510 | roundTemp(hours[i].temperature, &temp[0]);
511 | if (i == 0) {
512 | pr(bar_x, bar_y + bar_height + short_tick + 12, hour, fontSmall);
513 | pr(bar_x, bar_y + bar_height + short_tick + 36, temp, fontMedium);
514 | } else {
515 | centre(bar_x, bar_y + bar_height + short_tick + 12, hour, fontSmall);
516 | centre(bar_x, bar_y + bar_height + short_tick + 36, temp, fontMedium);
517 | }
518 | ink.print("\xba");
519 | }
520 |
521 | if (last_x == -1) {
522 | strcpy(last, hours[i].icon);
523 | last_x = bar_x;
524 | } else {
525 | if (strcmp(last, hours[i].icon) != 0) {
526 | ink.drawThickLine(bar_x, bar_y, bar_x, bar_y + bar_height, 0, 1);
527 | centreIcon(last_x + (bar_x - last_x) / 2, bar_y + bar_height / 2, last, SMALL_IMAGE);
528 | strcpy(last, hours[i].icon);
529 | last_x = bar_x;
530 | }
531 | }
532 |
533 | bar_x += bar_hour_spacing;
534 | if ((i % 24) == 23) {
535 | centreIcon(last_x + (bar_x - last_x) / 2, bar_y + bar_height / 2, last, SMALL_IMAGE);
536 | bar_x = bar_start_x;
537 | bar_y += bar_height + bar_gap;
538 | last_x = -1;
539 | }
540 | }
541 |
542 | bar_width /= 2;
543 | bar_width /= 7;
544 | int16_t bar_day_spacing = bar_width;
545 | bar_width *= 7;
546 |
547 | bar_x = bar_start_x;
548 | drawRectangle(bar_x, bar_y, bar_width, bar_height);
549 | pr(bar_x, bar_y - 7, "Next 7 Days", fontMedium);
550 |
551 | int count = 0;
552 | JsonObject daily = doc["daily"];
553 | for (JsonObject daily_data_item : daily["data"].as()) {
554 | ink.drawThickLine(bar_x, bar_y, bar_x, bar_y + bar_height, 0, 1);
555 |
556 | const char* icon = daily_data_item["icon"];
557 | centreIcon(bar_x + bar_day_spacing / 2, bar_y + bar_height / 2, icon, SMALL_IMAGE);
558 |
559 | uint32_t when = daily_data_item["time"];
560 | centre(bar_x + bar_day_spacing / 2, bar_y + bar_height + 22, day(when, offset), fontMedium);
561 |
562 | char high[TEMP_SIZE];
563 | roundTemp(daily_data_item["temperatureHigh"], &high[0]);
564 | char low[TEMP_SIZE];
565 | roundTemp(daily_data_item["temperatureLow"], &low[0]);
566 |
567 | // The reason the degree symbol is printed separately from the actual temperature is
568 | // that this causes the temperature digits to be centered and looks better. If the
569 | // symbol is included in the centred string it doesn't look as good on screen.
570 |
571 | centre(bar_x + bar_day_spacing / 2, bar_y + 21, high, fontMedium);
572 | ink.print("\xba");
573 | centre(bar_x + bar_day_spacing / 2, bar_y + bar_height - 5, low, fontMedium);
574 | ink.print("\xba");
575 |
576 | bar_x += bar_day_spacing;
577 | count += 1;
578 | if (count == 7) {
579 | break;
580 | }
581 | }
582 |
583 | // Step 5.
584 | //
585 | // Now make another API call to just get the minute-by-minute rain for the current
586 | // hour and draw that on screen.
587 |
588 | response = callAPI("daily,hourly,alerts", 0);
589 | if (response == "") {
590 | return;
591 | }
592 |
593 | doc.clear();
594 | err = deserializeJson(doc, response);
595 |
596 | if (err) {
597 | fatal("Deserialize JSON failed " + String(err.c_str()));
598 | return;
599 | }
600 |
601 | int16_t bar_spacing = 50;
602 | int16_t new_width = bar_width - bar_spacing;
603 | new_width /= 60;
604 | new_width *= 60;
605 | bar_x = bar_start_x + bar_width + bar_spacing + (bar_width - new_width - bar_spacing);
606 | bar_width = new_width;
607 | int16_t rain_width = bar_width / 60;
608 | drawRectangle(bar_x, bar_y, bar_width, bar_height);
609 | pr(bar_x, bar_y - 6, "Rain Next 60 Minutes", fontMedium);
610 |
611 | float max_rain = 4;
612 |
613 | count = 0;
614 | int16_t centre_x;
615 | JsonObject minutely = doc["minutely"];
616 | for (JsonObject minutely_data_item : minutely["data"].as()) {
617 | float rain = minutely_data_item["precipIntensity"];
618 | if (rain > max_rain) {
619 | rain = max_rain;
620 | }
621 |
622 | if (rain > 0) {
623 | ink.drawThickLine(bar_x, bar_y, bar_x, bar_y + (bar_height * (rain / max_rain)), 0, 1);
624 | }
625 |
626 | if ((count % 10) == 0) {
627 | int when = minutely_data_item["time"];
628 | char temp[TEMP_SIZE];
629 | hhmm(when, offset, &temp[0]);
630 | if (count == 0) {
631 | pr(bar_x, bar_y + bar_height + short_tick + 11, temp, fontSmall);
632 | } else if (count == 60) {
633 | right(bar_x, bar_y + bar_height + short_tick + 11, temp, fontSmall);
634 | } else {
635 | centre(bar_x, bar_y + bar_height + short_tick + 11, temp, fontSmall);
636 | }
637 | if (count == 30) {
638 | centre_x = bar_x;
639 | }
640 | ink.drawThickLine(bar_x, bar_y + bar_height, bar_x, bar_y + bar_height + short_tick, 0, 1);
641 | }
642 |
643 | count += 1;
644 | bar_x += rain_width;
645 | }
646 |
647 | // Step 6.
648 | //
649 | // Using data about the current forecast show the title and when the forecast
650 | // was last checked and local observations (weather and temperature).
651 |
652 | JsonObject currently = doc["currently"];
653 | const char *icon = currently["icon"];
654 | float c = currently["temperature"];
655 | uint32_t when = currently["time"];
656 |
657 | char temp[TEMP_SIZE];
658 | roundTemp(c, &temp[0]);
659 | pr(200, 80, title, fontLarge);
660 | char hm[HHMM_SIZE];
661 | hhmm(update_time, offset, &hm[0]);
662 | char subtitle[80];
663 | sprintf(subtitle, "Forecast checked at %s. It's %s\xba right now.", hm, temp);
664 |
665 | pr(200, 150, subtitle, fontLarge);
666 | centreIcon(bar_start_x + 64, 100, icon, LARGE_IMAGE);
667 |
668 | // Step 7.
669 | //
670 | // Add the status bar at the bottom
671 |
672 | char hmnow[HHMM_SIZE];
673 | hhmm(update_time, offset, &hmnow[0]);
674 | char hmnext[HHMM_SIZE];
675 | hhmm(next, offset, &hmnext[0]);
676 |
677 | JsonObject flags = doc["flags"];
678 | const char *version = flags["version"];
679 |
680 | char status_bar[255];
681 | sprintf(status_bar, "Updated: %s - Next update: %s - Time zone: %s (%.1f) - Temperature: %d\xba - Battery: %.1fv - API %s",
682 | hmnow, hmnext, tz, offset, ink.readTemperature(), ink.readBattery(), version);
683 | centre(ink.width() / 2, ink.height() - 75, status_bar, fontSmall);
684 |
685 | show();
686 | }
687 |
688 | // clear clears the display
689 | void clear() {
690 | ink.clearDisplay();
691 | ink.display();
692 | }
693 |
694 | // show displays whatever has been written to the display on the
695 | // e-ink screen itself.
696 | void show() {
697 | ink.display();
698 | }
699 |
700 | // fatal is used to show a fatal error on screen
701 | void fatal(String s) {
702 | clear();
703 | ink.setTextColor(0, 7);
704 | ink.setTextSize(4);
705 | ink.setCursor(100, 100);
706 | ink.print(s);
707 | show();
708 | }
709 |
710 | // deepSleep puts the device into deep sleep mode for sleep_time
711 | // seconds. When it wakes up setup() will be called.
712 | void deepSleep(uint64_t sleep_time) {
713 |
714 | // This is needed for Inkplate 10's that use the ESP32 WROVER-E
715 | // in order to reduce power consumption during sleep.
716 |
717 | rtc_gpio_isolate(GPIO_NUM_12);
718 |
719 | // The following sets the wake up timer to run after the appropriate
720 | // interval (in microseconds) and then goes to sleep.
721 |
722 | esp_sleep_enable_timer_wakeup(sleep_time * 1000000);
723 | esp_deep_sleep_start();
724 | }
725 |
--------------------------------------------------------------------------------