diff --git a/README.md b/README.md index ad7fdb4..2a183cd 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ A composed map can be pushed to the screen, saved to SD or used for further comp Downloaded tiles are cached in psram for reuse. This library should work on any ESP32 type with psram and a LovyanGFX compatible display. -OSM tiles are quite large -128kB per tile- so psram is required. +OSM tiles are quite large at 128kB or insane large at 512kB per tile, so psram is required. This project is not endorsed by or affiliated with the OpenStreetMap Foundation. Use of any OSMF provided service is governed by the [OSMF Terms of Use](https://osmfoundation.org/wiki/Terms_of_Use). @@ -40,43 +40,103 @@ framework = arduino lib_deps = celliesprojects/OpenStreetMap-esp32@^1.0.6 lovyan03/LovyanGFX@^1.2.7 - https://github.com/bitbank2/PNGdec@^1.1.3 + bitbank2/PNGdec@^1.1.3 ``` +## Functions + ### Set map size ```c++ -void setSize(uint16_t w, uint16_t h); +void setSize(uint16_t w, uint16_t h) ``` -- If no size is set a 320px by 240px map will be returned by `fetchMap`. +- If no size is set a 320px by 240px map will be returned. +- The tile cache should be freed with `freeTilesCache()` after setting a new bigger map size. -### Resize cache +### Get the number of tiles needed to cache a map ```c++ -bool resizeTilesCache(uint16_t numberOfTiles); +uint16_t tilesNeeded(uint16_t w, uint16_t h) ``` -- If the cache is not resized before the first call to `fetchMap`, it will auto initialize with space for 10 tiles on the first call. -- Each tile allocates 128 kB psram. -- The cache content is cleared before resizing. +This returns the number of tiles required to cache the given map size. -### Free the memory used by the tile cache +### Resize the tiles cache ```c++ -void freeTilesCache(); +bool resizeTilesCache(uint16_t numberOfTiles) ``` +- If the cache is not resized before the first call to `fetchMap`, the cache will be auto initialized. +- The cache content is cleared before resizing. +- Each 256px tile allocates **128kB** psram. +- Each 512px tile allocates **512kB** psram. + +**Don't over-allocate the cache** +When resizing the cache, keep in mind that the map sprite also uses psram. +The PNG decoders -~50kB for each core- also live in psram. +Use the above `tilesNeeded` function to calculate a safe and sane cache size. + ### Fetch a map ```c++ -bool fetchMap(LGFX_Sprite &map, double longitude, double latitude, uint8_t zoom); +bool fetchMap(LGFX_Sprite &map, double longitude, double latitude, uint8_t zoom) ``` - Overflowing `longitude` are wrapped and normalized to +-180°. - Overflowing `latitude` are clamped to +-90°. - Valid range for the `zoom` level is 1-18. +### Free the memory used by the tile cache + +```c++ +void freeTilesCache() +``` + +### Switch to a different tile provider + +```c++ +bool setTileProvider(int index) +``` + +This function will switch to a tile provider (if) that is user defined in `src/TileProvider.hpp`. + +- Returns `true` and clears the cache on success. +- Returns `false` -and the current tile provider is unchanged- if no provider at the index is defined. + +### Get the number of defined providers + +`OSM_TILEPROVIDERS` gives the number of defined providers. + +Example use: + +```c++ +const int numberOfProviders = OSM_TILEPROVIDERS; +``` + +In the default setup there is only one provider defined. +See `src/TileProvider.hpp` for example setups for [https://www.thunderforest.com/](https://www.thunderforest.com/) that only require an API key and commenting/uncommenting 2 lines. +Registration and a hobby tier are available for free. + +### Get the provider name + +```c++ +char *getProviderName() +``` + +### Get the minimum zoom level + +```c++ +int getMinZoom() +``` + +### Get the maximum zoom level + +```c++ +int getMaxZoom() +``` + ## Example code ### Example returning the default 320x240 map diff --git a/src/CachedTile.hpp b/src/CachedTile.hpp index f6f3227..683ed48 100644 --- a/src/CachedTile.hpp +++ b/src/CachedTile.hpp @@ -50,9 +50,9 @@ struct CachedTile free(); } - bool allocate() + bool allocate(int tileSize) { - buffer = static_cast(heap_caps_malloc(256 * 256 * sizeof(uint16_t), MALLOC_CAP_SPIRAM)); + buffer = static_cast(heap_caps_malloc(tileSize * tileSize * sizeof(uint16_t), MALLOC_CAP_SPIRAM)); return buffer != nullptr; } diff --git a/src/OpenStreetMap-esp32.cpp b/src/OpenStreetMap-esp32.cpp index d6b84ab..33559bf 100644 --- a/src/OpenStreetMap-esp32.cpp +++ b/src/OpenStreetMap-esp32.cpp @@ -87,26 +87,26 @@ void OpenStreetMap::computeRequiredTiles(double longitude, double latitude, uint const int32_t targetTileY = static_cast(exactTileY); // Compute the offset inside the tile for the given coordinates - const int16_t targetOffsetX = (exactTileX - targetTileX) * OSM_TILESIZE; - const int16_t targetOffsetY = (exactTileY - targetTileY) * OSM_TILESIZE; + const int16_t targetOffsetX = (exactTileX - targetTileX) * currentProvider->tileSize; + const int16_t targetOffsetY = (exactTileY - targetTileY) * currentProvider->tileSize; // Compute the offset for tiles covering the map area to keep the location centered const int16_t tilesOffsetX = mapWidth / 2 - targetOffsetX; const int16_t tilesOffsetY = mapHeight / 2 - targetOffsetY; // Compute number of colums required - const float colsLeft = 1.0 * tilesOffsetX / OSM_TILESIZE; - const float colsRight = float(mapWidth - (tilesOffsetX + OSM_TILESIZE)) / OSM_TILESIZE; + const float colsLeft = 1.0 * tilesOffsetX / currentProvider->tileSize; + const float colsRight = float(mapWidth - (tilesOffsetX + currentProvider->tileSize)) / currentProvider->tileSize; numberOfColums = ceil(colsLeft) + 1 + ceil(colsRight); - startOffsetX = tilesOffsetX - (ceil(colsLeft) * OSM_TILESIZE); + startOffsetX = tilesOffsetX - (ceil(colsLeft) * currentProvider->tileSize); // Compute number of rows required - const float rowsTop = 1.0 * tilesOffsetY / OSM_TILESIZE; - const float rowsBottom = float(mapHeight - (tilesOffsetY + OSM_TILESIZE)) / OSM_TILESIZE; + const float rowsTop = 1.0 * tilesOffsetY / currentProvider->tileSize; + const float rowsBottom = float(mapHeight - (tilesOffsetY + currentProvider->tileSize)) / currentProvider->tileSize; const uint32_t numberOfRows = ceil(rowsTop) + 1 + ceil(rowsBottom); - startOffsetY = tilesOffsetY - (ceil(rowsTop) * OSM_TILESIZE); + startOffsetY = tilesOffsetY - (ceil(rowsTop) * currentProvider->tileSize); log_v(" Need %i * %i tiles. First tile offset is %d,%d", numberOfColums, numberOfRows, startOffsetX, startOffsetY); @@ -171,17 +171,11 @@ bool OpenStreetMap::isTileCachedOrBusy(uint32_t x, uint32_t y, uint8_t z) void OpenStreetMap::freeTilesCache() { - for (auto &tile : tilesCache) - tile.free(); - - tilesCache.clear(); + std::vector().swap(tilesCache); } bool OpenStreetMap::resizeTilesCache(uint16_t numberOfTiles) { - if (tilesCache.size() == numberOfTiles) - return true; - if (!numberOfTiles) { log_e("Invalid cache size: %d", numberOfTiles); @@ -193,7 +187,7 @@ bool OpenStreetMap::resizeTilesCache(uint16_t numberOfTiles) for (auto &tile : tilesCache) { - if (!tile.allocate()) + if (!tile.allocate(currentProvider->tileSize)) { log_e("Tile cache allocation failed!"); freeTilesCache(); @@ -211,7 +205,7 @@ void OpenStreetMap::updateCache(const tileList &requiredTiles, uint8_t zoom) if (!jobs.empty()) { runJobs(jobs); - log_i("Updated %i tiles in %lu ms - %i ms/tile", jobs.size(), millis() - startMS, (millis() - startMS) / jobs.size()); + log_d("Updated %i tiles in %lu ms - %i ms/tile", jobs.size(), millis() - startMS, (millis() - startMS) / jobs.size()); } } @@ -270,8 +264,8 @@ bool OpenStreetMap::composeMap(LGFX_Sprite &mapSprite, const tileList &requiredT continue; } - int drawX = startOffsetX + (tileIndex % numberOfColums) * OSM_TILESIZE; - int drawY = startOffsetY + (tileIndex / numberOfColums) * OSM_TILESIZE; + int drawX = startOffsetX + (tileIndex % numberOfColums) * currentProvider->tileSize; + int drawY = startOffsetY + (tileIndex / numberOfColums) * currentProvider->tileSize; auto it = std::find_if(tilesCache.begin(), tilesCache.end(), [&](const CachedTile &tile) @@ -280,7 +274,7 @@ bool OpenStreetMap::composeMap(LGFX_Sprite &mapSprite, const tileList &requiredT }); if (it != tilesCache.end()) - mapSprite.pushImage(drawX, drawY, OSM_TILESIZE, OSM_TILESIZE, it->buffer); + mapSprite.pushImage(drawX, drawY, currentProvider->tileSize, currentProvider->tileSize, it->buffer); else log_w("Tile (z=%d, x=%d, y=%d) not found in cache", zoom, tileX, tileY); @@ -293,8 +287,8 @@ bool OpenStreetMap::composeMap(LGFX_Sprite &mapSprite, const tileList &requiredT mapSprite.setTextColor(TFT_WHITE, TFT_BLACK); else mapSprite.setTextColor(TFT_BLACK); - mapSprite.drawRightString(" Map data from OpenStreetMap.org ", - mapSprite.width(), mapSprite.height() - 10, &DejaVu9); + mapSprite.drawRightString(currentProvider->attribution, + mapSprite.width(), mapSprite.height() - 10, &DejaVu9Modded); mapSprite.setTextColor(TFT_WHITE, TFT_BLACK); return true; @@ -308,7 +302,7 @@ bool OpenStreetMap::fetchMap(LGFX_Sprite &mapSprite, double longitude, double la return false; } - if (!zoom || zoom > OSM_MAX_ZOOM) + if (zoom < currentProvider->minZoom || zoom > currentProvider->maxZoom) { log_e("Invalid zoom level: %d", zoom); return false; @@ -320,14 +314,10 @@ bool OpenStreetMap::fetchMap(LGFX_Sprite &mapSprite, double longitude, double la return false; } - if (!tilesCache.capacity()) + if (!tilesCache.capacity() && !resizeTilesCache(tilesNeeded(mapWidth, mapHeight))) { - log_w("Cache not initialized, setting up a default cache..."); - if (!resizeTilesCache(OSM_DEFAULT_CACHE_ITEMS)) - { - log_e("Could not allocate tile cache"); - return false; - } + log_e("Could not allocate tile cache"); + return false; } longitude = fmod(longitude + 180.0, 360.0) - 180.0; @@ -366,7 +356,7 @@ bool OpenStreetMap::fillBuffer(WiFiClient *stream, MemoryBuffer &buffer, size_t result = "Timeout: " + String(OSM_TILE_TIMEOUT_MS) + " ms"; return false; } - vTaskDelay(pdMS_TO_TICKS(1)); + taskYIELD(); continue; } @@ -382,7 +372,7 @@ bool OpenStreetMap::fillBuffer(WiFiClient *stream, MemoryBuffer &buffer, size_t lastReadTime = millis(); } else - vTaskDelay(pdMS_TO_TICKS(1)); + taskYIELD(); } return true; } @@ -432,22 +422,25 @@ std::optional> OpenStreetMap::urlToBuffer(const ch void OpenStreetMap::PNGDraw(PNGDRAW *pDraw) { - uint16_t *destRow = currentInstance->currentTileBuffer + (pDraw->y * OSM_TILESIZE); + uint16_t *destRow = currentInstance->currentTileBuffer + (pDraw->y * currentInstance->currentProvider->tileSize); getPNGCurrentCore()->getLineAsRGB565(pDraw, destRow, PNG_RGB565_BIG_ENDIAN, 0xffffffff); } bool OpenStreetMap::fetchTile(CachedTile &tile, uint32_t x, uint32_t y, uint8_t zoom, String &result) { - char url[64]; - snprintf(url, sizeof(url), "https://tile.openstreetmap.org/%u/%u/%u.png", - static_cast(zoom), - static_cast(x), - static_cast(y)); - - const auto buffer = urlToBuffer(url, result); + String url = currentProvider->urlTemplate; + url.replace("{x}", String(x)); + url.replace("{y}", String(y)); + url.replace("{z}", String(zoom)); + if (currentProvider->requiresApiKey && strstr(url.c_str(), "{apiKey}")) + url.replace("{apiKey}", currentProvider->apiKey); + + const auto buffer = urlToBuffer(url.c_str(), result); if (!buffer) return false; + url.clear(); + PNG *png = getPNGCurrentCore(); const int16_t rc = png->openRAM(buffer.value()->get(), buffer.value()->size(), PNGDraw); if (rc != PNG_SUCCESS) @@ -456,7 +449,7 @@ bool OpenStreetMap::fetchTile(CachedTile &tile, uint32_t x, uint32_t y, uint8_t return false; } - if (png->getWidth() != OSM_TILESIZE || png->getHeight() != OSM_TILESIZE) + if (png->getWidth() != currentProvider->tileSize || png->getHeight() != currentProvider->tileSize) { result = "Unexpected tile size: w=" + String(png->getWidth()) + " h=" + String(png->getHeight()); return false; @@ -468,7 +461,7 @@ bool OpenStreetMap::fetchTile(CachedTile &tile, uint32_t x, uint32_t y, uint8_t const int decodeResult = png->decode(0, PNG_FAST_PALETTE); if (decodeResult != PNG_SUCCESS) { - result = "Decoding " + String(url) + " failed with code: " + String(decodeResult); + result = "Decoding " + url + " failed with code: " + String(decodeResult); tile.valid = false; return false; } @@ -550,3 +543,25 @@ bool OpenStreetMap::startTileWorkerTasks() log_i("Started %d tile worker task(s)", numberOfWorkers); return true; } + +uint16_t OpenStreetMap::tilesNeeded(uint16_t mapWidth, uint16_t mapHeight) +{ + const int tileSize = currentProvider->tileSize; + int tilesX = (mapWidth + tileSize - 1) / tileSize + 1; + int tilesY = (mapHeight + tileSize - 1) / tileSize + 1; + return tilesX * tilesY; +} + +bool OpenStreetMap::setTileProvider(int index) +{ + if (index < 0 || index >= OSM_TILEPROVIDERS) + { + log_e("invalid provider index"); + return false; + } + + currentProvider = &tileProviders[index]; + freeTilesCache(); + log_i("provider changed to '%s'", currentProvider->name); + return true; +} diff --git a/src/OpenStreetMap-esp32.hpp b/src/OpenStreetMap-esp32.hpp index 0ee9d8f..89adfc4 100644 --- a/src/OpenStreetMap-esp32.hpp +++ b/src/OpenStreetMap-esp32.hpp @@ -33,16 +33,15 @@ #include #include +#include "TileProvider.hpp" #include "CachedTile.hpp" #include "TileJob.hpp" #include "MemoryBuffer.hpp" #include "HTTPClientRAII.hpp" +#include "fonts/DejaVu9-modded.h" -constexpr uint16_t OSM_TILESIZE = 256; -constexpr uint16_t OSM_TILE_TIMEOUT_MS = 2500; -constexpr uint16_t OSM_DEFAULT_CACHE_ITEMS = 10; -constexpr uint16_t OSM_MAX_ZOOM = 18; -constexpr UBaseType_t OSM_TASK_PRIORITY = 10; +constexpr uint16_t OSM_TILE_TIMEOUT_MS = 500; +constexpr UBaseType_t OSM_TASK_PRIORITY = 1; constexpr uint32_t OSM_TASK_STACKSIZE = 5120; constexpr uint32_t OSM_JOB_QUEUE_SIZE = 50; constexpr bool OSM_FORCE_SINGLECORE = false; @@ -88,19 +87,22 @@ class OpenStreetMap ~OpenStreetMap(); void setSize(uint16_t w, uint16_t h); + uint16_t tilesNeeded(uint16_t mapWidth, uint16_t mapHeight); bool resizeTilesCache(uint16_t numberOfTiles); - void freeTilesCache(); bool fetchMap(LGFX_Sprite &sprite, double longitude, double latitude, uint8_t zoom); + inline void freeTilesCache(); + + bool setTileProvider(int index); + const char *getProviderName() { return currentProvider->name; }; + int getMinZoom() const { return currentProvider->minZoom; }; + int getMaxZoom() const { return currentProvider->maxZoom; }; private: - std::vector tilesCache; - static inline OpenStreetMap *currentInstance = nullptr; - static inline thread_local uint16_t *currentTileBuffer = nullptr; - static void PNGDraw(PNGDRAW *pDraw); double lon2tile(double lon, uint8_t zoom); double lat2tile(double lat, uint8_t zoom); void computeRequiredTiles(double longitude, double latitude, uint8_t zoom, tileList &requiredTiles); void updateCache(const tileList &requiredTiles, uint8_t zoom); + bool startTileWorkerTasks(); void makeJobList(const tileList &requiredTiles, std::vector &jobs, uint8_t zoom); void runJobs(const std::vector &jobs); CachedTile *findUnusedTile(const tileList &requiredTiles, uint8_t zoom); @@ -110,8 +112,13 @@ class OpenStreetMap bool fillBuffer(WiFiClient *stream, MemoryBuffer &buffer, size_t contentSize, String &result); bool composeMap(LGFX_Sprite &mapSprite, const tileList &requiredTiles, uint8_t zoom); static void tileFetcherTask(void *param); + static void PNGDraw(PNGDRAW *pDraw); + + static inline OpenStreetMap *currentInstance = nullptr; + static inline thread_local uint16_t *currentTileBuffer = nullptr; + const TileProvider *currentProvider = &tileProviders[0]; + std::vector tilesCache; TaskHandle_t ownerTask = nullptr; - bool startTileWorkerTasks(); int numberOfWorkers = 0; QueueHandle_t jobQueue = nullptr; diff --git a/src/TileProvider.hpp b/src/TileProvider.hpp new file mode 100644 index 0000000..6f23c41 --- /dev/null +++ b/src/TileProvider.hpp @@ -0,0 +1,86 @@ +/* + Copyright (c) 2025 Cellie https://github.com/CelliesProjects/OpenStreetMap-esp32 + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + SPDX-License-Identifier: MIT + */ + +#ifndef TILEPROVIDER_HPP_ +#define TILEPROVIDER_HPP_ + +struct TileProvider +{ + const char *name; + const char *urlTemplate; + const char *attribution; + bool requiresApiKey; + const char *apiKey; + int maxZoom; + int minZoom; + int tileSize; +}; + +const TileProvider osmStandard = { + "OSM Standard", + "https://tile.openstreetmap.org/{z}/{x}/{y}.png", + "© OpenStreetMap contributors", + false, + "", + 19, 0, 256}; + +const TileProvider ThunderTransportDark256 = { + "Thunderforest Transport Dark 256px", + "https://tile.thunderforest.com/transport-dark/{z}/{x}/{y}.png?apikey={apiKey}", + "© Thunderforest, OpenStreetMap contributors", + true, + "YOUR_THUNDERFOREST_KEY", + 22, 0, 256}; + +const TileProvider ThunderForestCycle512 = { + "Thunderforest Cycle 512px", + "https://tile.thunderforest.com/cycle/{z}/{x}/{y}@2x.png?apikey={apiKey}", + "© Thunderforest, OpenStreetMap contributors", + true, + "YOUR_THUNDERFOREST_KEY", + 22, 0, 512}; + +const TileProvider ThunderForestCycle256 = { + "Thunderforest Cycle 256px", + "https://tile.thunderforest.com/cycle/{z}/{x}/{y}.png?apikey={apiKey}", + "© Thunderforest, OpenStreetMap contributors", + true, + "YOUR_THUNDERFOREST_KEY", + 22, 0, 256}; + +// Replace 'YOUR_THUNDERFOREST_KEY' above with a -free- Thunderforest API key +// and uncomment one of the following line to use Thunderforest tiles + +// const TileProvider tileProviders[] = {osmStandard, ThunderTransportDark256, ThunderForestCycle512, ThunderForestCycle256}; +// const TileProvider tileProviders[] = {ThunderTransportDark256}; +// const TileProvider tileProviders[] = {ThunderForestCycle512}; +// const TileProvider tileProviders[] = {ThunderForestCycle256}; + +// If one of the above definitions is used, the following line should be commented out +const TileProvider tileProviders[] = {osmStandard}; + +constexpr int OSM_TILEPROVIDERS = sizeof(tileProviders) / sizeof(TileProvider); + +static_assert(OSM_TILEPROVIDERS > 0, "No TileProvider configured"); + +#endif diff --git a/src/fonts/DejaVu9-modded.h b/src/fonts/DejaVu9-modded.h new file mode 100644 index 0000000..06c3e8c --- /dev/null +++ b/src/fonts/DejaVu9-modded.h @@ -0,0 +1,186 @@ +/* DejaVu 9 + original ttf url : https://dejavu-fonts.github.io/ + original license : https://dejavu-fonts.github.io/License.html +This data has been converted to AdafruitGFX font format from DejaVuSans.ttf. + +Modded by Cellie - added a © sign 0xA9 with https://tchapi.github.io/Adafruit-GFX-Font-Customiser/ +*/ +const uint8_t DejaVu9Bitmaps[] PROGMEM = { + 0xFA, 0xFA, 0xB4, 0x28, 0xAF, 0xCA, 0xFD, 0x45, 0x00, 0x21, 0xEA, 0x38, + 0x38, 0xAF, 0x08, 0x44, 0xA4, 0xA8, 0x5A, 0x15, 0x25, 0x22, 0x31, 0x04, + 0x19, 0x9E, 0x66, 0xC0, 0xC0, 0x4A, 0xA1, 0x85, 0x52, 0xAB, 0x9D, 0x50, + 0x21, 0x3E, 0x42, 0x00, 0xC0, 0xC0, 0x80, 0x25, 0x25, 0x20, 0x69, 0x99, + 0x99, 0x60, 0xC9, 0x24, 0xB8, 0x64, 0x84, 0x44, 0x43, 0xC0, 0x69, 0x16, + 0x11, 0x60, 0x11, 0x94, 0xA9, 0x7C, 0x40, 0xF8, 0x8E, 0x11, 0xE0, 0x7C, + 0x8E, 0x99, 0x60, 0xF1, 0x22, 0x24, 0x40, 0x69, 0x96, 0x99, 0x60, 0x69, + 0x97, 0x13, 0xE0, 0x88, 0x8C, 0x04, 0xEE, 0x0E, 0x04, 0xFC, 0x0F, 0xC0, + 0x81, 0xC1, 0xDC, 0x80, 0xE1, 0x24, 0x40, 0x40, 0x3C, 0x42, 0x9D, 0xA5, + 0xA5, 0x9E, 0x40, 0x38, 0x30, 0xC4, 0x92, 0x7A, 0x18, 0x40, 0xF4, 0x63, + 0xE8, 0xC7, 0xC0, 0x72, 0x61, 0x08, 0x25, 0xC0, 0xF4, 0xE3, 0x18, 0xCF, + 0xC0, 0xF8, 0x8F, 0x88, 0xF0, 0xF8, 0x8F, 0x88, 0x80, 0x76, 0x61, 0x38, + 0xE5, 0xC0, 0x8C, 0x63, 0xF8, 0xC6, 0x20, 0xFE, 0x55, 0x55, 0x80, 0x8C, + 0xA9, 0x8A, 0x4A, 0x20, 0x88, 0x88, 0x88, 0xF0, 0x87, 0x3C, 0xED, 0xB6, + 0x18, 0x40, 0x8E, 0x73, 0x59, 0xCE, 0x20, 0x76, 0xE3, 0x18, 0xED, 0xC0, + 0xE9, 0x9E, 0x88, 0x80, 0x76, 0xE3, 0x18, 0xE9, 0xC2, 0xE4, 0xA5, 0xCA, + 0x4A, 0x20, 0x72, 0x28, 0x1C, 0x0A, 0x27, 0x00, 0xF9, 0x08, 0x42, 0x10, + 0x80, 0x8C, 0x63, 0x18, 0xC5, 0xC0, 0x86, 0x14, 0x92, 0x48, 0xC3, 0x00, + 0x49, 0x24, 0x8A, 0x85, 0x43, 0xE0, 0xA0, 0x50, 0xCD, 0x23, 0x0C, 0x31, + 0x28, 0xC0, 0x8A, 0x9C, 0x42, 0x10, 0x80, 0xF8, 0x44, 0x44, 0x43, 0xE0, + 0xEA, 0xAB, 0x91, 0x24, 0x48, 0xD5, 0x57, 0x31, 0x20, 0xF8, 0x90, 0x61, + 0x79, 0xF0, 0x88, 0x8E, 0x99, 0x9E, 0x78, 0x88, 0x70, 0x11, 0x17, 0x99, + 0x97, 0x69, 0xF8, 0x70, 0x34, 0x4E, 0x44, 0x44, 0x79, 0x99, 0x71, 0x60, + 0x88, 0x8E, 0x99, 0x99, 0xBE, 0x45, 0x55, 0x80, 0x88, 0x89, 0xAC, 0xA9, + 0xFF, 0xED, 0x26, 0x4C, 0x99, 0x20, 0xE9, 0x99, 0x90, 0x69, 0x99, 0x60, + 0xE9, 0x99, 0xE8, 0x80, 0x79, 0x99, 0x71, 0x10, 0xF2, 0x48, 0x68, 0x62, + 0xE0, 0x4F, 0x44, 0x47, 0x99, 0x99, 0x70, 0x44, 0x98, 0xA1, 0xC1, 0x00, + 0x93, 0x76, 0xBA, 0x24, 0x40, 0x8A, 0x88, 0xA8, 0x80, 0x44, 0x88, 0xA1, + 0xC1, 0x02, 0x18, 0x00, 0xF1, 0x24, 0xF0, 0x69, 0x64, 0x93, 0xFF, 0x80, + 0xC9, 0x34, 0x96, 0x01, 0x91, 0x80, 0x3C, 0x42, 0x99, 0xA5, 0xA1, 0xA5, + 0x99, 0x42, 0x3C +}; + +const GFXglyph DejaVu9Glyphs[] PROGMEM = { + { 0, 0, 0, 4, 0, 1 }, // 0x20 ' ' + { 1, 1, 7, 4, 1, -6 }, // 0x21 '!' + { 2, 3, 2, 5, 1, -6 }, // 0x22 '"' + { 3, 6, 7, 9, 1, -6 }, // 0x23 '#' + { 9, 6, 8, 7, 0, -6 }, // 0x24 '$' + { 15, 8, 7, 10, 0, -6 }, // 0x25 '%' + { 22, 6, 7, 9, 1, -6 }, // 0x26 '&' + { 28, 1, 2, 3, 1, -6 }, // 0x27 ''' + { 29, 2, 8, 5, 1, -7 }, // 0x28 '(' + { 31, 2, 8, 5, 1, -7 }, // 0x29 ')' + { 33, 5, 4, 6, 0, -6 }, // 0x2A '*' + { 36, 5, 5, 9, 1, -4 }, // 0x2B '+' + { 40, 1, 2, 4, 1, 0 }, // 0x2C ',' + { 41, 2, 1, 4, 1, -2 }, // 0x2D '-' + { 42, 1, 1, 4, 1, 0 }, // 0x2E '.' + { 43, 3, 7, 4, 0, -6 }, // 0x2F '/' + { 46, 4, 7, 7, 1, -6 }, // 0x30 '0' + { 50, 3, 7, 7, 2, -6 }, // 0x31 '1' + { 53, 5, 7, 7, 1, -6 }, // 0x32 '2' + { 58, 4, 7, 7, 1, -6 }, // 0x33 '3' + { 62, 5, 7, 7, 1, -6 }, // 0x34 '4' + { 67, 4, 7, 7, 1, -6 }, // 0x35 '5' + { 71, 4, 7, 7, 1, -6 }, // 0x36 '6' + { 75, 4, 7, 7, 1, -6 }, // 0x37 '7' + { 79, 4, 7, 7, 1, -6 }, // 0x38 '8' + { 83, 4, 7, 7, 1, -6 }, // 0x39 '9' + { 87, 1, 5, 4, 1, -4 }, // 0x3A ':' + { 88, 1, 6, 4, 1, -4 }, // 0x3B ';' + { 89, 6, 5, 9, 1, -4 }, // 0x3C '<' + { 93, 6, 3, 9, 1, -3 }, // 0x3D '=' + { 96, 6, 5, 9, 1, -4 }, // 0x3E '>' + { 100, 4, 7, 6, 1, -6 }, // 0x3F '?' + { 104, 8, 8, 11, 1, -6 }, // 0x40 '@' + { 112, 6, 7, 7, 0, -6 }, // 0x41 'A' + { 118, 5, 7, 8, 1, -6 }, // 0x42 'B' + { 123, 5, 7, 8, 1, -6 }, // 0x43 'C' + { 128, 5, 7, 8, 1, -6 }, // 0x44 'D' + { 133, 4, 7, 7, 1, -6 }, // 0x45 'E' + { 137, 4, 7, 7, 1, -6 }, // 0x46 'F' + { 141, 5, 7, 8, 1, -6 }, // 0x47 'G' + { 146, 5, 7, 8, 1, -6 }, // 0x48 'H' + { 151, 1, 7, 4, 1, -6 }, // 0x49 'I' + { 152, 2, 9, 4, 0, -6 }, // 0x4A 'J' + { 155, 5, 7, 7, 1, -6 }, // 0x4B 'K' + { 160, 4, 7, 6, 1, -6 }, // 0x4C 'L' + { 164, 6, 7, 9, 1, -6 }, // 0x4D 'M' + { 170, 5, 7, 8, 1, -6 }, // 0x4E 'N' + { 175, 5, 7, 8, 1, -6 }, // 0x4F 'O' + { 180, 4, 7, 7, 1, -6 }, // 0x50 'P' + { 184, 5, 8, 8, 1, -6 }, // 0x51 'Q' + { 189, 5, 7, 7, 1, -6 }, // 0x52 'R' + { 194, 6, 7, 8, 1, -6 }, // 0x53 'S' + { 200, 5, 7, 6, 0, -6 }, // 0x54 'T' + { 205, 5, 7, 8, 1, -6 }, // 0x55 'U' + { 210, 6, 7, 7, 0, -6 }, // 0x56 'V' + { 216, 9, 7, 8, -1, -6 }, // 0x57 'W' + { 224, 6, 7, 7, 0, -6 }, // 0x58 'X' + { 230, 5, 7, 6, 0, -6 }, // 0x59 'Y' + { 235, 5, 7, 6, 0, -6 }, // 0x5A 'Z' + { 240, 2, 8, 5, 1, -6 }, // 0x5B '[' + { 242, 3, 7, 4, 0, -6 }, // 0x5C '\' + { 245, 2, 8, 5, 1, -6 }, // 0x5D ']' + { 247, 6, 2, 9, 1, -6 }, // 0x5E '^' + { 249, 5, 1, 6, 0, 2 }, // 0x5F '_' + { 250, 2, 2, 6, 1, -7 }, // 0x60 '`' + { 251, 4, 5, 7, 1, -4 }, // 0x61 'a' + { 254, 4, 8, 7, 1, -7 }, // 0x62 'b' + { 258, 4, 5, 7, 1, -4 }, // 0x63 'c' + { 261, 4, 8, 7, 1, -7 }, // 0x64 'd' + { 265, 4, 5, 7, 1, -4 }, // 0x65 'e' + { 268, 4, 8, 4, 0, -7 }, // 0x66 'f' + { 272, 4, 7, 7, 1, -4 }, // 0x67 'g' + { 276, 4, 8, 7, 1, -7 }, // 0x68 'h' + { 280, 1, 7, 4, 1, -6 }, // 0x69 'i' + { 281, 2, 9, 4, 0, -6 }, // 0x6A 'j' + { 284, 4, 8, 6, 1, -7 }, // 0x6B 'k' + { 288, 1, 8, 4, 1, -7 }, // 0x6C 'l' + { 289, 7, 5, 10, 1, -4 }, // 0x6D 'm' + { 294, 4, 5, 7, 1, -4 }, // 0x6E 'n' + { 297, 4, 5, 7, 1, -4 }, // 0x6F 'o' + { 300, 4, 7, 7, 1, -4 }, // 0x70 'p' + { 304, 4, 7, 7, 1, -4 }, // 0x71 'q' + { 308, 3, 5, 5, 1, -4 }, // 0x72 'r' + { 310, 4, 5, 6, 1, -4 }, // 0x73 's' + { 313, 4, 6, 5, 0, -5 }, // 0x74 't' + { 316, 4, 5, 7, 1, -4 }, // 0x75 'u' + { 319, 7, 5, 6, -1, -4 }, // 0x76 'v' + { 324, 7, 5, 8, 0, -4 }, // 0x77 'w' + { 329, 5, 5, 6, 0, -4 }, // 0x78 'x' + { 333, 7, 7, 6, -1, -4 }, // 0x79 'y' + { 340, 4, 5, 7, 1, -4 }, // 0x7A 'z' + { 343, 3, 8, 6, 1, -6 }, // 0x7B '{' + { 346, 1, 9, 4, 1, -6 }, // 0x7C '|' + { 348, 3, 8, 6, 1, -6 }, // 0x7D '}' + { 351, 6, 3, 9, 1, -4 }, // 0x7E '~' + { 0, 0, 0, 0, 0, 0 }, // 0x7F 'non-printable' + { 0, 0, 0, 0, 0, 0 }, // 0x80 'non-printable' + { 0, 0, 0, 0, 0, 0 }, // 0x81 'non-printable' + { 0, 0, 0, 0, 0, 0 }, // 0x82 'non-printable' + { 0, 0, 0, 0, 0, 0 }, // 0x83 'non-printable' + { 0, 0, 0, 0, 0, 0 }, // 0x84 'non-printable' + { 0, 0, 0, 0, 0, 0 }, // 0x85 'non-printable' + { 0, 0, 0, 0, 0, 0 }, // 0x86 'non-printable' + { 0, 0, 0, 0, 0, 0 }, // 0x87 'non-printable' + { 0, 0, 0, 0, 0, 0 }, // 0x88 'non-printable' + { 0, 0, 0, 0, 0, 0 }, // 0x89 'non-printable' + { 0, 0, 0, 0, 0, 0 }, // 0x8A 'non-printable' + { 0, 0, 0, 0, 0, 0 }, // 0x8B 'non-printable' + { 0, 0, 0, 0, 0, 0 }, // 0x8C 'non-printable' + { 0, 0, 0, 0, 0, 0 }, // 0x8D 'non-printable' + { 0, 0, 0, 0, 0, 0 }, // 0x8E 'non-printable' + { 0, 0, 0, 0, 0, 0 }, // 0x8F 'non-printable' + { 0, 0, 0, 0, 0, 0 }, // 0x90 'non-printable' + { 0, 0, 0, 0, 0, 0 }, // 0x91 'non-printable' + { 0, 0, 0, 0, 0, 0 }, // 0x92 'non-printable' + { 0, 0, 0, 0, 0, 0 }, // 0x93 'non-printable' + { 0, 0, 0, 0, 0, 0 }, // 0x94 'non-printable' + { 0, 0, 0, 0, 0, 0 }, // 0x95 'non-printable' + { 0, 0, 0, 0, 0, 0 }, // 0x96 'non-printable' + { 0, 0, 0, 0, 0, 0 }, // 0x97 'non-printable' + { 0, 0, 0, 0, 0, 0 }, // 0x98 'non-printable' + { 0, 0, 0, 0, 0, 0 }, // 0x99 'non-printable' + { 0, 0, 0, 0, 0, 0 }, // 0x9A 'non-printable' + { 0, 0, 0, 0, 0, 0 }, // 0x9B 'non-printable' + { 0, 0, 0, 0, 0, 0 }, // 0x9C 'non-printable' + { 0, 0, 0, 0, 0, 0 }, // 0x9D 'non-printable' + { 0, 0, 0, 0, 0, 0 }, // 0x9E 'non-printable' + { 0, 0, 0, 0, 0, 0 }, // 0x9F 'non-printable' + { 0, 0, 0, 0, 0, 0 }, // 0xA0 ' ' + { 0, 0, 0, 0, 0, 0 }, // 0xA1 '¡' + { 0, 0, 0, 0, 0, 0 }, // 0xA2 '¢' + { 0, 0, 0, 0, 0, 0 }, // 0xA3 '£' + { 0, 0, 0, 0, 0, 0 }, // 0xA4 '¤' + { 0, 0, 0, 0, 0, 0 }, // 0xA5 '¥' + { 0, 0, 0, 0, 0, 0 }, // 0xA6 '¦' + { 0, 0, 0, 0, 0, 0 }, // 0xA7 '§' + { 0, 0, 0, 0, 0, 0 }, // 0xA8 '¨' + { 354, 8, 9, 10, 1, -7 } // 0xA9 '©' +}; + +const GFXfont DejaVu9Modded PROGMEM = { + (uint8_t*)DejaVu9Bitmaps, + (GFXglyph*)DejaVu9Glyphs, 0x20, 0xA9, 10 }; +