├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── README.md ├── assets ├── icons │ ├── lpehacker.png │ ├── lpenote.png │ ├── lpesip.png │ ├── lpetantrum.png │ └── lpethink.png └── images │ ├── bg_bubble.png │ ├── bg_cat.png │ ├── bg_lpe_bubble.png │ ├── bg_pablo.png │ ├── bg_splash.png │ ├── bg_stonks.png │ └── bg_what_a_week.png ├── docs ├── front.jpg ├── led_shroud.png ├── makerworld_logo.png ├── select_preset.jpg ├── timer_paused.jpg └── timer_running.jpg ├── platformio.ini ├── poetry.lock ├── pyproject.toml ├── scripts └── gen_assets.py ├── src ├── GxEPD2_display_selection_new_style.h ├── GxEPD2_selection_check.h ├── anniversary.h ├── button.cpp ├── button.h ├── checkbox.cpp ├── checkbox.h ├── debug.h ├── defs.h ├── fonts │ ├── FunnelDisplay_Bold18pt7b.h │ ├── FunnelDisplay_Bold24pt7b.h │ ├── FunnelDisplay_Bold32pt7b.h │ ├── FunnelDisplay_Bold48pt7b.h │ ├── FunnelDisplay_Bold60pt7b.h │ ├── FunnelDisplay_Regular14pt7b.h │ ├── HelvetiPixel16pt7b.h │ └── HelvetiPixel24pt7b.h ├── gfx_utils.cpp ├── gfx_utils.h ├── icon.h ├── icon_provider.cpp ├── icon_provider.h ├── icons.h ├── icons │ └── icons.cpp ├── images.h ├── images │ ├── image_bg_bubble.h │ ├── image_bg_cat.h │ ├── image_bg_lpe_bubble.h │ ├── image_bg_lpehacker_bubble.h │ ├── image_bg_pablo.h │ ├── image_bg_splash.h │ ├── image_bg_stonks.h │ └── image_bg_what_a_week.h ├── led.cpp ├── led.h ├── lpe.h ├── main.cpp ├── menu.cpp ├── menu.h ├── preferences_manager.cpp ├── preferences_manager.h ├── splashscreen.cpp ├── splashscreen.h ├── states │ ├── timer_running.cpp │ ├── timer_running_break.cpp │ ├── timer_selecting_preset.cpp │ └── timer_waiting_for_confirmation.cpp ├── statistics.cpp ├── statistics.h ├── strings.cpp ├── strings.h ├── timer.cpp └── timer.h ├── stl ├── Pomodoro - Cover.stl ├── Pomodoro - Housing.stl ├── Pomodoro - Knob.stl └── Pomodoro - LEDCase.stl ├── test └── README.md └── uv.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | .vscode/.browse.c_cpp.db* 3 | .vscode/c_cpp_properties.json 4 | .vscode/launch.json 5 | .vscode/ipch 6 | 7 | assets/fonts/*.ttf 8 | assets/icons/*.png 9 | !assets/icons/lpe*.png 10 | 11 | src/strings_private.cpp 12 | src/anniversary.cpp 13 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "platformio.platformio-ide" 6 | ], 7 | "unwantedRecommendations": [ 8 | "ms-vscode.cpptools-extension-pack" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "*.tcc": "cpp", 4 | "algorithm": "cpp", 5 | "array": "cpp", 6 | "string": "cpp", 7 | "string_view": "cpp", 8 | "vector": "cpp", 9 | "iosfwd": "cpp", 10 | "functional": "cpp", 11 | "new": "cpp", 12 | "optional": "cpp", 13 | "istream": "cpp", 14 | "ostream": "cpp", 15 | "system_error": "cpp", 16 | "tuple": "cpp", 17 | "type_traits": "cpp", 18 | "utility": "cpp" 19 | } 20 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![the finished product from the front](docs/front.jpg) 2 | 3 | This is the repository for an ESP32 based focus timer. It uses an ePaper display and a rotary dial for input. 4 | The code in this repository will not be ready-to-use, as some assets and fonts have been removed. However, if you really want to you should be able to adapt the code to your needs. 5 | 6 |

7 | 8 | MakerWorld Logo 9 |
10 | View on MakerWorld 11 |
12 |

13 | 14 | ## Parts List 15 | 16 | - ESP32 (I used an [AZDelivery ESP32 NodeMCU](https://www.az-delivery.de/en/products/esp32-developmentboard)) 17 | - WaveShare 4.26inch e-Paper display HAT, 800x480 ([link](https://www.waveshare.com/4.26inch-e-paper-hat.htm)) 18 | - Other displays will work but the UI is designed for this specific resolution 19 | - KY-040 rotary encoder with button 20 | - A single WS2812 LED (could be replaced with a simple RGB LED) 21 | - A USB-C connector (like [this one](https://amzn.eu/d/8UpvqWe)) 22 | - Note: if you use a 2-wired USB-C connector, you might need to use an USB A to USB C cable to power the device (my best guess is because of the missing power delivery negotiation) 23 | - 3d printed case ([`onshape` file](https://cad.onshape.com/documents/06055e629740267835bb7660/w/df56eb93ab74e2f4d61e5097/e/21a7853695e4900200750891?renderMode=0&uiState=67e6e3924368850ba92069f6)) 24 | - Some resistors (for the LED and a pullup resistor for the switch) and 0.1uF capacitors (to smooth out the rotary encoder signal) 25 | - Optional: tire balancing weights and rubber feet 26 | 27 | ## Project Origin 28 | 29 | I love trying out different productivity techniques - some say that the quest to optimize your productivity is the ultimate procrastination method, so maybe that is what drove me to this project. I also have a habit of committing time (around a month of work outside my normal job) once a year to a project that benefits someone else. Last year, I bought a 3D printer (BambuLab X1C) and wanted to put it to good use. I have previously finished an apprenticeship as an electronics 30 | engineer before pivoting to software engineering, so I also wanted to come back to my roots and build something physical. 31 | My friend struggles with time management throughout the day sometimes - lots of different tasks to organize, and little focus. So I thought to myself: Why not make them a focus timer? So, I set out with a few goals: 32 | 33 | - It should be a physical device 34 | - It should be _fun_ 35 | - It should be intuitive to use 36 | 37 | There are some cool projects out there (arguably much cooler than this, for example the [Focus Dial by Salim Benbouziyane](https://www.youtube.com/watch?v=nZa-Vqu-_fU)), but I wanted to build something from scratch. I also 38 | never built something with an ePaper display and thought it might be a good fit for something that is mostly idling and doesn't require a backlight. 39 | 40 | ### Why these parts? 41 | 42 | This was my second dive back into building things with microcontrollers in a long time. I knew ESP32 well enough to feel comfortable diving back in, so that was the main choice here. I did some research before to see what kinds of displays would be supported. 43 | 44 | #### ePaper Display 45 | 46 | I needed some sort of display, or at least I _wanted_ some sort of display. One of the main motivations for this project was that it should be out of your way - until it is time to finish your current focus and move on. For me, this meant that I wanted a display without any backlight. 47 | 48 | The display should also be large enough that you can put the whole device somewhere on your desk and still be able to read it. After ordering and playing around with a few WaveShare ePaper displays, I settled on the 4.26" variant for multiple reasons: 49 | 50 | - Great resolution (which seems to be really hard to find for "hobbyist" displays) 51 | - The size felt right 52 | - The display supports partial refreshes (0.3s, no distracting "black and white flashes" while refreshing) 53 | 54 | Initially, I really wanted to use a black/white/red display and found one that I liked, but the refresh time 55 | was a whopping 16 seconds with no support for partial refreshes which was a dealbreaker for me. 56 | 57 | The final bonus feature: it won't work at night. If your desk is not bright enough, you won't be able to read the display. This is a feature, not a bug. Too dark outside? Stop working already! 58 | 59 | #### Rotary Encoder 60 | 61 | From the start, I knew that I wanted some sort of dial as an input - it made the most sense to me. This came at the cost of some complexity when designing the menus, and you really need to make sure that you debounce the input correctly. I also added .1uF capacitors to the CLK and DT pins to help with smoothing out the signal. 62 | 63 | #### LED 64 | 65 | In the first few iterations, there was no plan for an LED. My genius plan of having a display without backlight came at a cost: it could be _too_ subtle when your current focus time ended. I experimented with a few different ideas: 66 | 67 | - A buzzer: this would just make you jump. A truly horrible experience 68 | - Speakers: I don't know why, but speakers felt _hard_. So much noise and whining with the setup I tried, but I will blame this on a skill issue 69 | - LED: I had some WS2812 LEDs lying around and thought they might be a good fit. You can animate them with the NeoPixel library, and they are really easy to use. The additional benefit of not needing to commit many more output pins was also a big plus 70 | 71 | ![LED shroud screenshot of the CAD model](docs/led_shroud.png) 72 | 73 | The LED ended up working great, allowing me to display different states. It might be subtle, but I also added a little shroud to the case and added a diffusion layer in front of the LED to make it look nicer. 74 | 75 | ### Building the Case 76 | 77 | The case comes in two parts: the base and a lid. One unfortunate design choice I made is that the display frame is printed as one piece as part of the base, so the top edge tends to warp a little bit during printing. Since CAD (or product design) isn't my strongest suit, there will certainly be better choices to design this for a better final look. 78 | 79 | One thing that I wished I learned earlier is that it might not have been the best idea to put the dial in the front: because the print and electronics are so lightweight, pressing the switch on the dial will tend to just slide the whole device back. Luckily, I could solve this by adding some rubber feet and weights (the ones usually used to balance tires) to the bottom of the case. This worked out great, and I am happy with how it turned out. 80 | 81 | ### Software 82 | 83 | The software is written in C++ and uses the Arduino framework. I used PlatformIO to manage the project (at least that is what seemed to be a popular choice, but I am not so sure about that anymore). This project relies heavily 84 | on the GxEPD2 library for the display. I won't lie, the code in this repository is a bit of a mess - I had to get things done in time, which led to quite a bit of copy and pasting and not revisiting earlier parts of the code. 85 | Some parts were generated by AI (Claude, for the most part) to help me finish the project in the deadline I set myself. 86 | 87 | ![a random fact displayed on the screen](docs/timer_running.jpg) 88 | 89 | Since this was a project for my friend, I also wanted to include some easter eggs and fun. You would think that adding some random facts _while you are supposed to be focused_ would be a bad idea, but I think it is a fun little addition. 90 | 91 | ## Using the Device 92 | 93 | When the device starts up, you can either change some settings or go into preset selection mode. From there, you can choose one of three presets: 94 | 95 | ![preset selection](docs/select_preset.jpg) 96 | 97 | The timer will then start and let you know once the time is up (by flashing the LED and displaying a message on the screen). You can keep working (not recommended, but necessary if you want to finish something) and then start the break. 98 | 99 | ![timer running](docs/timer_running.jpg) 100 | 101 | During the pause, you can view some statistics. Every few iterations (4 by default), your pause will be longer to give you some time to recover. 102 | 103 | ![pause statistics](docs/timer_paused.jpg) 104 | 105 | ## Development 106 | 107 | ### Prerequisites 108 | 109 | - PlatformIO (I used the VSCode extension) 110 | - Python 3.13+ for asset (re)generation 111 | 112 | ### Generating Assets 113 | 114 | In order to prepare images, icons, and fonts, you will need to run the `generate_assets.py` script. This script will take care of resizing images, converting them to the correct format, and generating the necessary C++ code to include them in the project. 115 | 116 | ```bash 117 | # install dependencies with uv or a different package manager 118 | uv sync 119 | 120 | uv run scripts/generate_assets.py 121 | ``` 122 | 123 | ### Customizing Presets 124 | 125 | The presets are defined in `src/main.cpp`: 126 | 127 | ```cpp 128 | timer.addPreset(iconProvider->getPresetIcon("Emails"), iconProvider->getTimerRunningBackgroundImage(), "Emails", 15 * MINUTE, 5 * MINUTE, 15 * MINUTE); 129 | timer.addPreset(iconProvider->getPresetIcon("Coding"), iconProvider->getTimerRunningBackgroundImage(), "Coding", 45 * MINUTE, 15 * MINUTE, 30 * MINUTE, 2); 130 | timer.addPreset(iconProvider->getPresetIcon("Focus"), iconProvider->getTimerRunningBackgroundImage(), "Focus", 25 * MINUTE, 5 * MINUTE, 20 * MINUTE); 131 | ``` 132 | 133 | If you want to customize this, I would start there and keep looking for references to these presets. 134 | 135 | ## Pin Mapping 136 | 137 | #### Rotary Encoder (KY-040) 138 | 139 | | PIN | # | 140 | | --- | --- | 141 | | CLK | 32 | 142 | | DT | 21 | 143 | | SW | 14 | 144 | 145 | #### ePaper Display (GxEPD2_426_GDEQ0426T82, WaveShare 4.26" b/w) 146 | 147 | | PIN | # | 148 | | ---- | --- | 149 | | BUSY | 4 | 150 | | RST | 16 | 151 | | DC | 17 | 152 | | CS | 5 | 153 | | CLK | 18 | 154 | | DIN | 23 | 155 | 156 | #### LED (WS2812) 157 | 158 | | PIN | # | 159 | | --- | --- | 160 | | DIN | 25 | 161 | -------------------------------------------------------------------------------- /assets/icons/lpehacker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rukenshia/pomodoro/2c339876d54325ed6505e066731f024be315d8aa/assets/icons/lpehacker.png -------------------------------------------------------------------------------- /assets/icons/lpenote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rukenshia/pomodoro/2c339876d54325ed6505e066731f024be315d8aa/assets/icons/lpenote.png -------------------------------------------------------------------------------- /assets/icons/lpesip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rukenshia/pomodoro/2c339876d54325ed6505e066731f024be315d8aa/assets/icons/lpesip.png -------------------------------------------------------------------------------- /assets/icons/lpetantrum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rukenshia/pomodoro/2c339876d54325ed6505e066731f024be315d8aa/assets/icons/lpetantrum.png -------------------------------------------------------------------------------- /assets/icons/lpethink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rukenshia/pomodoro/2c339876d54325ed6505e066731f024be315d8aa/assets/icons/lpethink.png -------------------------------------------------------------------------------- /assets/images/bg_bubble.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rukenshia/pomodoro/2c339876d54325ed6505e066731f024be315d8aa/assets/images/bg_bubble.png -------------------------------------------------------------------------------- /assets/images/bg_cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rukenshia/pomodoro/2c339876d54325ed6505e066731f024be315d8aa/assets/images/bg_cat.png -------------------------------------------------------------------------------- /assets/images/bg_lpe_bubble.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rukenshia/pomodoro/2c339876d54325ed6505e066731f024be315d8aa/assets/images/bg_lpe_bubble.png -------------------------------------------------------------------------------- /assets/images/bg_pablo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rukenshia/pomodoro/2c339876d54325ed6505e066731f024be315d8aa/assets/images/bg_pablo.png -------------------------------------------------------------------------------- /assets/images/bg_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rukenshia/pomodoro/2c339876d54325ed6505e066731f024be315d8aa/assets/images/bg_splash.png -------------------------------------------------------------------------------- /assets/images/bg_stonks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rukenshia/pomodoro/2c339876d54325ed6505e066731f024be315d8aa/assets/images/bg_stonks.png -------------------------------------------------------------------------------- /assets/images/bg_what_a_week.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rukenshia/pomodoro/2c339876d54325ed6505e066731f024be315d8aa/assets/images/bg_what_a_week.png -------------------------------------------------------------------------------- /docs/front.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rukenshia/pomodoro/2c339876d54325ed6505e066731f024be315d8aa/docs/front.jpg -------------------------------------------------------------------------------- /docs/led_shroud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rukenshia/pomodoro/2c339876d54325ed6505e066731f024be315d8aa/docs/led_shroud.png -------------------------------------------------------------------------------- /docs/makerworld_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rukenshia/pomodoro/2c339876d54325ed6505e066731f024be315d8aa/docs/makerworld_logo.png -------------------------------------------------------------------------------- /docs/select_preset.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rukenshia/pomodoro/2c339876d54325ed6505e066731f024be315d8aa/docs/select_preset.jpg -------------------------------------------------------------------------------- /docs/timer_paused.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rukenshia/pomodoro/2c339876d54325ed6505e066731f024be315d8aa/docs/timer_paused.jpg -------------------------------------------------------------------------------- /docs/timer_running.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rukenshia/pomodoro/2c339876d54325ed6505e066731f024be315d8aa/docs/timer_running.jpg -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | ; PlatformIO Project Configuration File 2 | ; 3 | ; Build options: build flags, source filter 4 | ; Upload options: custom upload port, speed and extra flags 5 | ; Library options: dependencies, extra library storages 6 | ; Advanced options: extra scripting 7 | ; 8 | ; Please visit documentation for the other options and examples 9 | ; https://docs.platformio.org/page/projectconf.html 10 | 11 | [env:az-delivery-devkit-v4] 12 | platform = espressif32 13 | board = az-delivery-devkit-v4 14 | framework = arduino 15 | monitor_speed = 115200 16 | lib_deps = 17 | zinggjm/GxEPD2@^1.6.2 18 | madhephaestus/ESP32Encoder@^0.11.7 19 | makuna/NeoPixelBus@^2.8.3 20 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "pillow" 5 | version = "11.1.0" 6 | description = "Python Imaging Library (Fork)" 7 | optional = false 8 | python-versions = ">=3.9" 9 | groups = ["main"] 10 | files = [ 11 | {file = "pillow-11.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:e1abe69aca89514737465752b4bcaf8016de61b3be1397a8fc260ba33321b3a8"}, 12 | {file = "pillow-11.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c640e5a06869c75994624551f45e5506e4256562ead981cce820d5ab39ae2192"}, 13 | {file = "pillow-11.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a07dba04c5e22824816b2615ad7a7484432d7f540e6fa86af60d2de57b0fcee2"}, 14 | {file = "pillow-11.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e267b0ed063341f3e60acd25c05200df4193e15a4a5807075cd71225a2386e26"}, 15 | {file = "pillow-11.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bd165131fd51697e22421d0e467997ad31621b74bfc0b75956608cb2906dda07"}, 16 | {file = "pillow-11.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:abc56501c3fd148d60659aae0af6ddc149660469082859fa7b066a298bde9482"}, 17 | {file = "pillow-11.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:54ce1c9a16a9561b6d6d8cb30089ab1e5eb66918cb47d457bd996ef34182922e"}, 18 | {file = "pillow-11.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:73ddde795ee9b06257dac5ad42fcb07f3b9b813f8c1f7f870f402f4dc54b5269"}, 19 | {file = "pillow-11.1.0-cp310-cp310-win32.whl", hash = "sha256:3a5fe20a7b66e8135d7fd617b13272626a28278d0e578c98720d9ba4b2439d49"}, 20 | {file = "pillow-11.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:b6123aa4a59d75f06e9dd3dac5bf8bc9aa383121bb3dd9a7a612e05eabc9961a"}, 21 | {file = "pillow-11.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:a76da0a31da6fcae4210aa94fd779c65c75786bc9af06289cd1c184451ef7a65"}, 22 | {file = "pillow-11.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e06695e0326d05b06833b40b7ef477e475d0b1ba3a6d27da1bb48c23209bf457"}, 23 | {file = "pillow-11.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96f82000e12f23e4f29346e42702b6ed9a2f2fea34a740dd5ffffcc8c539eb35"}, 24 | {file = "pillow-11.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3cd561ded2cf2bbae44d4605837221b987c216cff94f49dfeed63488bb228d2"}, 25 | {file = "pillow-11.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f189805c8be5ca5add39e6f899e6ce2ed824e65fb45f3c28cb2841911da19070"}, 26 | {file = "pillow-11.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:dd0052e9db3474df30433f83a71b9b23bd9e4ef1de13d92df21a52c0303b8ab6"}, 27 | {file = "pillow-11.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:837060a8599b8f5d402e97197d4924f05a2e0d68756998345c829c33186217b1"}, 28 | {file = "pillow-11.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:aa8dd43daa836b9a8128dbe7d923423e5ad86f50a7a14dc688194b7be5c0dea2"}, 29 | {file = "pillow-11.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0a2f91f8a8b367e7a57c6e91cd25af510168091fb89ec5146003e424e1558a96"}, 30 | {file = "pillow-11.1.0-cp311-cp311-win32.whl", hash = "sha256:c12fc111ef090845de2bb15009372175d76ac99969bdf31e2ce9b42e4b8cd88f"}, 31 | {file = "pillow-11.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fbd43429d0d7ed6533b25fc993861b8fd512c42d04514a0dd6337fb3ccf22761"}, 32 | {file = "pillow-11.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:f7955ecf5609dee9442cbface754f2c6e541d9e6eda87fad7f7a989b0bdb9d71"}, 33 | {file = "pillow-11.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2062ffb1d36544d42fcaa277b069c88b01bb7298f4efa06731a7fd6cc290b81a"}, 34 | {file = "pillow-11.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a85b653980faad27e88b141348707ceeef8a1186f75ecc600c395dcac19f385b"}, 35 | {file = "pillow-11.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9409c080586d1f683df3f184f20e36fb647f2e0bc3988094d4fd8c9f4eb1b3b3"}, 36 | {file = "pillow-11.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fdadc077553621911f27ce206ffcbec7d3f8d7b50e0da39f10997e8e2bb7f6a"}, 37 | {file = "pillow-11.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:93a18841d09bcdd774dcdc308e4537e1f867b3dec059c131fde0327899734aa1"}, 38 | {file = "pillow-11.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9aa9aeddeed452b2f616ff5507459e7bab436916ccb10961c4a382cd3e03f47f"}, 39 | {file = "pillow-11.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3cdcdb0b896e981678eee140d882b70092dac83ac1cdf6b3a60e2216a73f2b91"}, 40 | {file = "pillow-11.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:36ba10b9cb413e7c7dfa3e189aba252deee0602c86c309799da5a74009ac7a1c"}, 41 | {file = "pillow-11.1.0-cp312-cp312-win32.whl", hash = "sha256:cfd5cd998c2e36a862d0e27b2df63237e67273f2fc78f47445b14e73a810e7e6"}, 42 | {file = "pillow-11.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a697cd8ba0383bba3d2d3ada02b34ed268cb548b369943cd349007730c92bddf"}, 43 | {file = "pillow-11.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:4dd43a78897793f60766563969442020e90eb7847463eca901e41ba186a7d4a5"}, 44 | {file = "pillow-11.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae98e14432d458fc3de11a77ccb3ae65ddce70f730e7c76140653048c71bfcbc"}, 45 | {file = "pillow-11.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cc1331b6d5a6e144aeb5e626f4375f5b7ae9934ba620c0ac6b3e43d5e683a0f0"}, 46 | {file = "pillow-11.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:758e9d4ef15d3560214cddbc97b8ef3ef86ce04d62ddac17ad39ba87e89bd3b1"}, 47 | {file = "pillow-11.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b523466b1a31d0dcef7c5be1f20b942919b62fd6e9a9be199d035509cbefc0ec"}, 48 | {file = "pillow-11.1.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:9044b5e4f7083f209c4e35aa5dd54b1dd5b112b108648f5c902ad586d4f945c5"}, 49 | {file = "pillow-11.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:3764d53e09cdedd91bee65c2527815d315c6b90d7b8b79759cc48d7bf5d4f114"}, 50 | {file = "pillow-11.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31eba6bbdd27dde97b0174ddf0297d7a9c3a507a8a1480e1e60ef914fe23d352"}, 51 | {file = "pillow-11.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b5d658fbd9f0d6eea113aea286b21d3cd4d3fd978157cbf2447a6035916506d3"}, 52 | {file = "pillow-11.1.0-cp313-cp313-win32.whl", hash = "sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9"}, 53 | {file = "pillow-11.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:593c5fd6be85da83656b93ffcccc2312d2d149d251e98588b14fbc288fd8909c"}, 54 | {file = "pillow-11.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:11633d58b6ee5733bde153a8dafd25e505ea3d32e261accd388827ee987baf65"}, 55 | {file = "pillow-11.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:70ca5ef3b3b1c4a0812b5c63c57c23b63e53bc38e758b37a951e5bc466449861"}, 56 | {file = "pillow-11.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8000376f139d4d38d6851eb149b321a52bb8893a88dae8ee7d95840431977081"}, 57 | {file = "pillow-11.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ee85f0696a17dd28fbcfceb59f9510aa71934b483d1f5601d1030c3c8304f3c"}, 58 | {file = "pillow-11.1.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:dd0e081319328928531df7a0e63621caf67652c8464303fd102141b785ef9547"}, 59 | {file = "pillow-11.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e63e4e5081de46517099dc30abe418122f54531a6ae2ebc8680bcd7096860eab"}, 60 | {file = "pillow-11.1.0-cp313-cp313t-win32.whl", hash = "sha256:dda60aa465b861324e65a78c9f5cf0f4bc713e4309f83bc387be158b077963d9"}, 61 | {file = "pillow-11.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ad5db5781c774ab9a9b2c4302bbf0c1014960a0a7be63278d13ae6fdf88126fe"}, 62 | {file = "pillow-11.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:67cd427c68926108778a9005f2a04adbd5e67c442ed21d95389fe1d595458756"}, 63 | {file = "pillow-11.1.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:bf902d7413c82a1bfa08b06a070876132a5ae6b2388e2712aab3a7cbc02205c6"}, 64 | {file = "pillow-11.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c1eec9d950b6fe688edee07138993e54ee4ae634c51443cfb7c1e7613322718e"}, 65 | {file = "pillow-11.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e275ee4cb11c262bd108ab2081f750db2a1c0b8c12c1897f27b160c8bd57bbc"}, 66 | {file = "pillow-11.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4db853948ce4e718f2fc775b75c37ba2efb6aaea41a1a5fc57f0af59eee774b2"}, 67 | {file = "pillow-11.1.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:ab8a209b8485d3db694fa97a896d96dd6533d63c22829043fd9de627060beade"}, 68 | {file = "pillow-11.1.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:54251ef02a2309b5eec99d151ebf5c9904b77976c8abdcbce7891ed22df53884"}, 69 | {file = "pillow-11.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5bb94705aea800051a743aa4874bb1397d4695fb0583ba5e425ee0328757f196"}, 70 | {file = "pillow-11.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89dbdb3e6e9594d512780a5a1c42801879628b38e3efc7038094430844e271d8"}, 71 | {file = "pillow-11.1.0-cp39-cp39-win32.whl", hash = "sha256:e5449ca63da169a2e6068dd0e2fcc8d91f9558aba89ff6d02121ca8ab11e79e5"}, 72 | {file = "pillow-11.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:3362c6ca227e65c54bf71a5f88b3d4565ff1bcbc63ae72c34b07bbb1cc59a43f"}, 73 | {file = "pillow-11.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:b20be51b37a75cc54c2c55def3fa2c65bb94ba859dde241cd0a4fd302de5ae0a"}, 74 | {file = "pillow-11.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8c730dc3a83e5ac137fbc92dfcfe1511ce3b2b5d7578315b63dbbb76f7f51d90"}, 75 | {file = "pillow-11.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:7d33d2fae0e8b170b6a6c57400e077412240f6f5bb2a342cf1ee512a787942bb"}, 76 | {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8d65b38173085f24bc07f8b6c505cbb7418009fa1a1fcb111b1f4961814a442"}, 77 | {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:015c6e863faa4779251436db398ae75051469f7c903b043a48f078e437656f83"}, 78 | {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d44ff19eea13ae4acdaaab0179fa68c0c6f2f45d66a4d8ec1eda7d6cecbcc15f"}, 79 | {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d3d8da4a631471dfaf94c10c85f5277b1f8e42ac42bade1ac67da4b4a7359b73"}, 80 | {file = "pillow-11.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:4637b88343166249fe8aa94e7c4a62a180c4b3898283bb5d3d2fd5fe10d8e4e0"}, 81 | {file = "pillow-11.1.0.tar.gz", hash = "sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20"}, 82 | ] 83 | 84 | [package.extras] 85 | docs = ["furo", "olefile", "sphinx (>=8.1)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] 86 | fpx = ["olefile"] 87 | mic = ["olefile"] 88 | tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "trove-classifiers (>=2024.10.12)"] 89 | typing = ["typing-extensions"] 90 | xmp = ["defusedxml"] 91 | 92 | [metadata] 93 | lock-version = "2.1" 94 | python-versions = ">=3.13" 95 | content-hash = "fe85fcbda1e6175d6a7bf926f2d5ea63838bcb5e6404c2353753bd532ac87b1e" 96 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "scripts" 3 | version = "0.1.0" 4 | description = "" 5 | authors = [ 6 | {name = "Rukenshia",email = "code@chrstphrsn.cc"} 7 | ] 8 | readme = "README.md" 9 | requires-python = ">=3.13" 10 | dependencies = [ 11 | "pillow (>=11.1.0,<12.0.0)" 12 | ] 13 | 14 | -------------------------------------------------------------------------------- /scripts/gen_assets.py: -------------------------------------------------------------------------------- 1 | """ 2 | This script processes the assets directory to generate relevant C files 3 | """ 4 | 5 | import os 6 | import sys 7 | import shutil 8 | from PIL import Image 9 | 10 | # Directories 11 | ICON_INPUT_DIR = sys.argv[1] if len(sys.argv) > 1 else "assets/icons" 12 | IMAGE_INPUT_DIR = "assets/images" # Background images (800x480 PNGs) 13 | OUTPUT_DIR = "src" 14 | ICONS_DIR = os.path.join(OUTPUT_DIR, "icons") 15 | IMAGES_DIR = os.path.join(OUTPUT_DIR, "images") 16 | ICONS_HEADER_FILE = os.path.join(OUTPUT_DIR, "icons.h") 17 | ICONS_SOURCE_FILE = os.path.join(ICONS_DIR, "icons.cpp") 18 | IMAGES_AGGREGATE_HEADER_FILE = os.path.join(OUTPUT_DIR, "images.h") 19 | 20 | 21 | def image_to_c_array(image): 22 | width, height = image.size 23 | byte_data = [] 24 | for y in range(height): 25 | byte_val, bit_count = 0, 0 26 | for x in range(width): 27 | if image.getpixel((x, y)) == 0: # BLACK pixel 28 | byte_val |= 1 << (7 - bit_count) 29 | bit_count += 1 30 | if bit_count == 8: 31 | byte_data.append(byte_val) 32 | byte_val, bit_count = 0, 0 33 | if bit_count > 0: 34 | byte_data.append(byte_val) 35 | return byte_data 36 | 37 | 38 | def process_icon_image(image_path): 39 | img = Image.open(image_path) 40 | if img.mode in ("RGBA", "LA"): 41 | img = img.convert("RGBA") 42 | new_img = Image.new("RGB", img.size, (255, 255, 255)) 43 | new_img.paste(img, mask=img.split()[3]) 44 | img = new_img.convert("L") 45 | else: 46 | img = img.convert("L") 47 | sizes = [192, 128, 64, 48] 48 | c_arrays = {} 49 | for size in sizes: 50 | resized = img.resize((size, size), Image.LANCZOS) 51 | bw_img = resized.convert("1", dither=Image.FLOYDSTEINBERG) 52 | c_arrays[size] = image_to_c_array(bw_img) 53 | return c_arrays 54 | 55 | 56 | def process_icons_directory(input_directory): 57 | if not os.path.exists(input_directory): 58 | print(f"❌ Directory '{input_directory}' does not exist.") 59 | return 60 | 61 | files = [ 62 | f for f in os.listdir(input_directory) if f.lower().endswith((".bmp", ".png")) 63 | ] 64 | if not files: 65 | print(f"⚠ No BMP or PNG files found in '{input_directory}'.") 66 | return 67 | 68 | os.makedirs(OUTPUT_DIR, exist_ok=True) 69 | os.makedirs(ICONS_DIR, exist_ok=True) 70 | 71 | icon_names = [] 72 | icon_data_entries = [] 73 | 74 | with open(ICONS_SOURCE_FILE, "w") as cpp_file: 75 | cpp_file.write('#include "icons.h"\n\n') 76 | for filename in files: 77 | file_path = os.path.join(input_directory, filename) 78 | icon_slug = os.path.splitext(filename)[0] 79 | print(f"🔄 Processing icon {filename}...") 80 | c_arrays = process_icon_image(file_path) 81 | icon_names.append(icon_slug) 82 | for size, data in c_arrays.items(): 83 | cpp_file.write( 84 | f"const unsigned char icon_{icon_slug}_{size}[] = {{\n " 85 | ) 86 | for i, byte in enumerate(data): 87 | cpp_file.write(f"0x{byte:02X}, ") 88 | if (i + 1) % 12 == 0: 89 | cpp_file.write("\n ") 90 | cpp_file.write("\n};\n\n") 91 | icon_data_entries.append( 92 | f"Icon icon_{icon_slug}(icon_{icon_slug}_192, icon_{icon_slug}_128, icon_{icon_slug}_64, icon_{icon_slug}_48);" 93 | ) 94 | cpp_file.write("\n".join(icon_data_entries)) 95 | cpp_file.write("\n") 96 | 97 | with open(ICONS_HEADER_FILE, "w") as header_file: 98 | header_file.write("#ifndef ICONS_H\n#define ICONS_H\n\n") 99 | header_file.write('#include "icon.h"\n\n') 100 | for icon_slug in icon_names: 101 | header_file.write(f"extern Icon icon_{icon_slug};\n") 102 | header_file.write("\n#endif // ICONS_H\n") 103 | 104 | print(f"✔ Icons header file saved: {ICONS_HEADER_FILE}") 105 | print(f"✔ Icons source file saved: {ICONS_SOURCE_FILE}") 106 | 107 | 108 | def process_background_image(image_path): 109 | img = Image.open(image_path) 110 | if img.mode in ("RGBA", "LA"): 111 | img = img.convert("RGBA") 112 | new_img = Image.new("RGB", img.size, (255, 255, 255)) 113 | new_img.paste(img, mask=img.split()[3]) 114 | img = new_img.convert("L") 115 | else: 116 | img = img.convert("L") 117 | # Background images are assumed to be 800x480; no resizing. 118 | bw_img = img.convert("1", dither=Image.FLOYDSTEINBERG) 119 | return image_to_c_array(bw_img) 120 | 121 | 122 | def process_images_directory(input_directory): 123 | if not os.path.exists(input_directory): 124 | print(f"❌ Directory '{input_directory}' does not exist.") 125 | return 126 | 127 | files = [f for f in os.listdir(input_directory) if f.lower().endswith(".png")] 128 | if not files: 129 | print(f"⚠ No PNG files found in '{input_directory}'.") 130 | return 131 | 132 | os.makedirs(OUTPUT_DIR, exist_ok=True) 133 | os.makedirs(IMAGES_DIR, exist_ok=True) 134 | 135 | image_slugs = [] 136 | 137 | for filename in files: 138 | file_path = os.path.join(input_directory, filename) 139 | image_slug = os.path.splitext(filename)[0] 140 | image_slugs.append(image_slug) 141 | print(f"🔄 Processing background image {filename}...") 142 | c_array = process_background_image(file_path) 143 | header_filename = f"image_{image_slug}.h" 144 | header_filepath = os.path.join(IMAGES_DIR, header_filename) 145 | include_guard = f"IMAGE_{image_slug.upper()}_H" 146 | with open(header_filepath, "w") as header_file: 147 | header_file.write(f"#ifndef {include_guard}\n#define {include_guard}\n\n") 148 | header_file.write(f"const unsigned char image_{image_slug}[] = {{\n ") 149 | for i, byte in enumerate(c_array): 150 | header_file.write(f"0x{byte:02X}, ") 151 | if (i + 1) % 12 == 0: 152 | header_file.write("\n ") 153 | header_file.write("\n};\n\n") 154 | header_file.write(f"#endif // {include_guard}\n") 155 | print(f"✔ Background image header saved: {header_filepath}") 156 | 157 | # Create a single aggregated header for all background images. 158 | with open(IMAGES_AGGREGATE_HEADER_FILE, "w") as agg_header: 159 | agg_header.write("#ifndef IMAGES_H\n#define IMAGES_H\n\n") 160 | for slug in image_slugs: 161 | agg_header.write(f'#include "images/image_{slug}.h"\n') 162 | agg_header.write("\n#endif // IMAGES_H\n") 163 | print(f"✔ Aggregated images header saved: {IMAGES_AGGREGATE_HEADER_FILE}") 164 | 165 | 166 | def process_font(font_name, sizes): 167 | """Process a font file in multiple sizes using fontconvert.""" 168 | font_path = os.path.join("assets/fonts", font_name) 169 | if not os.path.exists(font_path): 170 | print(f"❌ Font file not found: {font_path}") 171 | return 172 | 173 | output_dir = os.path.join(OUTPUT_DIR, "fonts") 174 | os.makedirs(output_dir, exist_ok=True) 175 | 176 | for size in sizes: 177 | # Replace hyphens with underscores in the output filename 178 | base_name = os.path.splitext(font_name)[0].replace("-", "_") 179 | output_name = f"{base_name}{size}pt7b" 180 | output_path = os.path.join(output_dir, f"{output_name}.h") 181 | 182 | # Run fontconvert tool 183 | cmd = f"fontconvert {font_path} {size} > {output_path}" 184 | ret = os.system(cmd) 185 | if ret == 0: 186 | print(f"✔ Generated font header: {output_path}") 187 | else: 188 | print(f"❌ Failed to generate font: {output_path}") 189 | 190 | 191 | def process_fonts(): 192 | print("🔄 Processing fonts...") 193 | 194 | # Clean up existing fonts directory 195 | fonts_dir = os.path.join(OUTPUT_DIR, "fonts") 196 | if os.path.exists(fonts_dir): 197 | print(f"🗑 Removing existing fonts directory: {fonts_dir}") 198 | shutil.rmtree(fonts_dir) 199 | 200 | # Process only the fonts that are actually used in the project 201 | fonts_to_process = { 202 | "FunnelDisplay-Regular.ttf": [14], 203 | "FunnelDisplay-Bold.ttf": [18, 24, 32, 48, 60], 204 | "HelvetiPixel.ttf": [16, 24], 205 | } 206 | 207 | for font_name, sizes in fonts_to_process.items(): 208 | process_font(font_name, sizes) 209 | 210 | 211 | # Process icons, background images, and fonts 212 | if __name__ == "__main__": 213 | process_icons_directory(ICON_INPUT_DIR) 214 | process_images_directory(IMAGE_INPUT_DIR) 215 | process_fonts() 216 | -------------------------------------------------------------------------------- /src/GxEPD2_display_selection_new_style.h: -------------------------------------------------------------------------------- 1 | // Display Library example for SPI e-paper panels from Dalian Good Display and boards from Waveshare. 2 | // Requires HW SPI and Adafruit_GFX. Caution: the e-paper panels require 3.3V supply AND data lines! 3 | // 4 | // Display Library based on Demo Example from Good Display: https://www.good-display.com/companyfile/32/ 5 | // 6 | // Author: Jean-Marc Zingg 7 | // 8 | // Version: see library.properties 9 | // 10 | // Library: https://github.com/ZinggJM/GxEPD2 11 | 12 | // Supporting Arduino Forum Topics (closed, read only): 13 | // Good Display ePaper for Arduino: https://forum.arduino.cc/t/good-display-epaper-for-arduino/419657 14 | // Waveshare e-paper displays with SPI: https://forum.arduino.cc/t/waveshare-e-paper-displays-with-spi/467865 15 | // 16 | // Add new topics in https://forum.arduino.cc/c/using-arduino/displays/23 for new questions and issues 17 | 18 | // NOTE: you may need to adapt or select for your wiring in the processor specific conditional compile sections below 19 | 20 | #ifndef GxEPD2_DISPLAYS_H 21 | #define GxEPD2_DISPLAYS_H 22 | 23 | // select the display class (only one), matching the kind of display panel 24 | #define GxEPD2_DISPLAY_CLASS GxEPD2_BW 25 | 26 | #define GxEPD2_DRIVER_CLASS GxEPD2_426_GDEQ0426T82 // GDEQ0426T82 480x800, SSD1677 (P426010-MF1-A) 27 | 28 | // SS is usually used for CS. define here for easy change 29 | #ifndef EPD_CS 30 | #define EPD_CS SS 31 | #endif 32 | 33 | #if defined(GxEPD2_DISPLAY_CLASS) && defined(GxEPD2_DRIVER_CLASS) 34 | 35 | // somehow there should be an easier way to do this 36 | #define GxEPD2_BW_IS_GxEPD2_BW true 37 | #define GxEPD2_3C_IS_GxEPD2_3C true 38 | #define GxEPD2_4C_IS_GxEPD2_4C true 39 | #define GxEPD2_7C_IS_GxEPD2_7C true 40 | #define GxEPD2_1248_IS_GxEPD2_1248 true 41 | #define GxEPD2_1248c_IS_GxEPD2_1248c true 42 | #define IS_GxEPD(c, x) (c##x) 43 | #define IS_GxEPD2_BW(x) IS_GxEPD(GxEPD2_BW_IS_, x) 44 | #define IS_GxEPD2_3C(x) IS_GxEPD(GxEPD2_3C_IS_, x) 45 | #define IS_GxEPD2_4C(x) IS_GxEPD(GxEPD2_4C_IS_, x) 46 | #define IS_GxEPD2_7C(x) IS_GxEPD(GxEPD2_7C_IS_, x) 47 | #define IS_GxEPD2_1248(x) IS_GxEPD(GxEPD2_1248_IS_, x) 48 | #define IS_GxEPD2_1248c(x) IS_GxEPD(GxEPD2_1248c_IS_, x) 49 | 50 | #include "GxEPD2_selection_check.h" 51 | 52 | // for my test use only 53 | // #if defined(_AVR) 54 | // #warning "defined(_AVR)" 55 | // #endif 56 | // #if defined(ESP32) 57 | // #warning "defined(ESP32)" 58 | // #endif 59 | // #if defined(ARDUINO_ARCH_ESP32) 60 | // #warning "defined(ARDUINO_ARCH_ESP32)" 61 | // #endif 62 | 63 | #if defined(ARDUINO_ARCH_AVR) 64 | #if defined(ARDUINO_AVR_MEGA2560) // Note: SS is on 53 on MEGA 65 | #define MAX_DISPLAY_BUFFER_SIZE 5000 // e.g. full height for 200x200 66 | #elif defined(ARDUINO_AVR_NANO_EVERY) 67 | #define EPD_CS 10 68 | #define MAX_DISPLAY_BUFFER_SIZE 5000 // e.g. full height for 200x200 69 | #else // Note: SS is on 10 on UNO, NANO 70 | #define MAX_DISPLAY_BUFFER_SIZE 800 // 71 | #endif 72 | #if IS_GxEPD2_BW(GxEPD2_DISPLAY_CLASS) 73 | #define MAX_HEIGHT(EPD) (EPD::HEIGHT <= MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8) ? EPD::HEIGHT : MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8)) 74 | #elif IS_GxEPD2_3C(GxEPD2_DISPLAY_CLASS) || IS_GxEPD2_4C(GxEPD2_DISPLAY_CLASS) 75 | #define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE / 2) / (EPD::WIDTH / 8) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE / 2) / (EPD::WIDTH / 8)) 76 | #elif IS_GxEPD2_7C(GxEPD2_DISPLAY_CLASS) 77 | #define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2)) 78 | #endif 79 | // adapt the constructor parameters to your wiring 80 | GxEPD2_DISPLAY_CLASS display(GxEPD2_DRIVER_CLASS(/*CS=*/EPD_CS, /*DC=*/8, /*RST=*/9, /*BUSY=*/7)); 81 | // for Arduino Micro or Arduino Leonardo with CS on 10 on my proto boards (SS would be 17) uncomment instead: 82 | // GxEPD2_DISPLAY_CLASS display(GxEPD2_DRIVER_CLASS(/*CS=*/ 10, /*DC=*/ 8, /*RST=*/ 9, /*BUSY=*/ 7)); 83 | #endif 84 | 85 | #if defined(ARDUINO_ARCH_MEGAAVR) 86 | #if defined(ARDUINO_AVR_NANO_EVERY) 87 | #define MAX_DISPLAY_BUFFER_SIZE 5000 // e.g. full height for 200x200 88 | #if IS_GxEPD2_BW(GxEPD2_DISPLAY_CLASS) 89 | #define MAX_HEIGHT(EPD) (EPD::HEIGHT <= MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8) ? EPD::HEIGHT : MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8)) 90 | #elif IS_GxEPD2_3C(GxEPD2_DISPLAY_CLASS) || IS_GxEPD2_4C(GxEPD2_DISPLAY_CLASS) 91 | #define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE / 2) / (EPD::WIDTH / 8) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE / 2) / (EPD::WIDTH / 8)) 92 | #elif IS_GxEPD2_7C(GxEPD2_DISPLAY_CLASS) 93 | #define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2)) 94 | #endif 95 | // adapt the constructor parameters to your wiring 96 | GxEPD2_DISPLAY_CLASS display(GxEPD2_DRIVER_CLASS(/*CS=*/10, /*DC=*/8, /*RST=*/9, /*BUSY=*/7)); 97 | #endif 98 | #endif 99 | 100 | #if defined(ARDUINO_ARCH_ESP32) 101 | #define MAX_DISPLAY_BUFFER_SIZE 65536ul // e.g. 102 | #if IS_GxEPD2_BW(GxEPD2_DISPLAY_CLASS) 103 | #define MAX_HEIGHT(EPD) (EPD::HEIGHT <= MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8) ? EPD::HEIGHT : MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8)) 104 | #elif IS_GxEPD2_3C(GxEPD2_DISPLAY_CLASS) || IS_GxEPD2_4C(GxEPD2_DISPLAY_CLASS) 105 | #define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE / 2) / (EPD::WIDTH / 8) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE / 2) / (EPD::WIDTH / 8)) 106 | #elif IS_GxEPD2_7C(GxEPD2_DISPLAY_CLASS) 107 | #define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2)) 108 | #endif 109 | // adapt the constructor parameters to your wiring 110 | #if !IS_GxEPD2_1248(GxEPD2_DRIVER_CLASS) && !IS_GxEPD2_1248c(GxEPD2_DRIVER_CLASS) 111 | #if defined(ARDUINO_NANO_ESP32) // uses Dx pin names 112 | GxEPD2_DISPLAY_CLASS display(GxEPD2_DRIVER_CLASS(/*CS=*/D10, /*DC=*/D8, /*RST=*/D9, /*BUSY=*/D7)); 113 | #elif defined(ARDUINO_LOLIN_D32_PRO) 114 | GxEPD2_DISPLAY_CLASS display(GxEPD2_DRIVER_CLASS(/*CS=5*/ EPD_CS, /*DC=*/0, /*RST=*/2, /*BUSY=*/15)); // my LOLIN_D32_PRO proto board 115 | #elif defined(ARDUINO_LOLIN_S2_MINI) 116 | GxEPD2_DISPLAY_CLASS display(GxEPD2_DRIVER_CLASS(/*CS*/ 33, /*DC=*/35, /*RST=*/37, /*BUSY=*/39)); // my LOLIN ESP32 S2 mini connection 117 | #else 118 | // GxEPD2_DISPLAY_CLASS display(GxEPD2_DRIVER_CLASS(/*CS=*/ 27, /*DC=*/ 14, /*RST=*/ 12, /*BUSY=*/ 13)); // Good Display ESP32 Development Kit ESP32-L 119 | // GxEPD2_DISPLAY_CLASS display(GxEPD2_DRIVER_CLASS(/*CS=*/ 27, /*DC=*/ 14, /*RST=*/ 12, /*BUSY=*/ 13, /*CS2=*/ 4)); // for GDEM1085T51 with ESP32-L 120 | GxEPD2_DISPLAY_CLASS display(GxEPD2_DRIVER_CLASS(/*CS=5*/ EPD_CS, /*DC=*/17, /*RST=*/16, /*BUSY=*/4)); // my suggested wiring and proto board 121 | // GxEPD2_DISPLAY_CLASS display(GxEPD2_DRIVER_CLASS(/*CS=5*/ 5, /*DC=*/ 17, /*RST=*/ 16, /*BUSY=*/ 4)); // LILYGO_T5_V2.4.1 122 | // GxEPD2_DISPLAY_CLASS display(GxEPD2_DRIVER_CLASS(/*CS=5*/ EPD_CS, /*DC=*/ 19, /*RST=*/ 4, /*BUSY=*/ 34)); // LILYGO® TTGO T5 2.66 123 | // GxEPD2_DISPLAY_CLASS display(GxEPD2_DRIVER_CLASS(/*CS=5*/ EPD_CS, /*DC=*/ 2, /*RST=*/ 0, /*BUSY=*/ 4)); // e.g. TTGO T8 ESP32-WROVER 124 | // GxEPD2_DISPLAY_CLASS display(GxEPD2_DRIVER_CLASS(/*CS=*/ 15, /*DC=*/ 27, /*RST=*/ 26, /*BUSY=*/ 25)); // Waveshare ESP32 Driver Board 125 | #endif 126 | #else // GxEPD2_1248 or GxEPD2_1248c 127 | // Waveshare 12.48 b/w or b/w/r SPI display board and frame or Good Display 12.48 b/w panel GDEW1248T3 or b/w/r panel GDEY1248Z51 128 | // general constructor for use with all parameters, e.g. for Waveshare ESP32 driver board mounted on connection board 129 | GxEPD2_DISPLAY_CLASS display(GxEPD2_DRIVER_CLASS(/*sck=*/13, /*miso=*/12, /*mosi=*/14, 130 | /*cs_m1=*/23, /*cs_s1=*/22, /*cs_m2=*/16, /*cs_s2=*/19, 131 | /*dc1=*/25, /*dc2=*/17, /*rst1=*/33, /*rst2=*/5, 132 | /*busy_m1=*/32, /*busy_s1=*/26, /*busy_m2=*/18, /*busy_s2=*/4)); 133 | #endif 134 | #undef MAX_DISPLAY_BUFFER_SIZE 135 | #undef MAX_HEIGHT 136 | #endif 137 | 138 | #if defined(ARDUINO_ARCH_ESP8266) 139 | #define MAX_DISPLAY_BUFFER_SIZE (81920ul - 34000ul - 5000ul) // ~34000 base use, change 5000 to your application use 140 | #if IS_GxEPD2_BW(GxEPD2_DISPLAY_CLASS) 141 | #define MAX_HEIGHT(EPD) (EPD::HEIGHT <= MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8) ? EPD::HEIGHT : MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8)) 142 | #elif IS_GxEPD2_3C(GxEPD2_DISPLAY_CLASS) || IS_GxEPD2_4C(GxEPD2_DISPLAY_CLASS) 143 | #define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE / 2) / (EPD::WIDTH / 8) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE / 2) / (EPD::WIDTH / 8)) 144 | #elif IS_GxEPD2_7C(GxEPD2_DISPLAY_CLASS) 145 | #define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2)) 146 | #endif 147 | // adapt the constructor parameters to your wiring 148 | GxEPD2_DISPLAY_CLASS display(GxEPD2_DRIVER_CLASS(/*CS=D8*/ EPD_CS, /*DC=D3*/ 0, /*RST=D4*/ 2, /*BUSY=D2*/ 4)); 149 | // mapping of Waveshare e-Paper ESP8266 Driver Board, new version 150 | // GxEPD2_DISPLAY_CLASS display(GxEPD2_DRIVER_CLASS(/*CS=15*/ EPD_CS, /*DC=4*/ 4, /*RST=2*/ 2, /*BUSY=5*/ 5)); 151 | // mapping of Waveshare e-Paper ESP8266 Driver Board, old version 152 | // GxEPD2_DISPLAY_CLASS display(GxEPD2_DRIVER_CLASS(/*CS=15*/ EPD_CS, /*DC=4*/ 4, /*RST=5*/ 5, /*BUSY=16*/ 16)); 153 | #undef MAX_DISPLAY_BUFFER_SIZE 154 | #undef MAX_HEIGHT 155 | #endif 156 | 157 | // can't use package "STMF1 Boards (STM32Duino.com)" (Roger Clark) anymore with Adafruit_GFX, use "STM32 Boards (selected from submenu)" (STMicroelectronics) 158 | #if defined(ARDUINO_ARCH_STM32) 159 | #define MAX_DISPLAY_BUFFER_SIZE 15000ul // ~15k is a good compromise 160 | #if IS_GxEPD2_BW(GxEPD2_DISPLAY_CLASS) 161 | #define MAX_HEIGHT(EPD) (EPD::HEIGHT <= MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8) ? EPD::HEIGHT : MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8)) 162 | #elif IS_GxEPD2_3C(GxEPD2_DISPLAY_CLASS) || IS_GxEPD2_4C(GxEPD2_DISPLAY_CLASS) 163 | #define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE / 2) / (EPD::WIDTH / 8) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE / 2) / (EPD::WIDTH / 8)) 164 | #elif IS_GxEPD2_7C(GxEPD2_DISPLAY_CLASS) 165 | #define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2)) 166 | #endif 167 | // adapt the constructor parameters to your wiring 168 | // for Good Display STM32 Development Kit DESPI-L. 169 | // needs jumpers from PA5 (PIN_SPI_SCK) to SCK for EPD and PA7 (PIN_SPI_MOSI) to SDI for EPD. PD9 and PD10 are not HW SPI capable. 170 | // GxEPD2_DISPLAY_CLASS display(GxEPD2_DRIVER_CLASS(/*CS=*/ PD8, /*DC=*/ PE15, /*RST=*/ PE14, /*BUSY=*/ PE13)); 171 | // GxEPD2_DISPLAY_CLASS display(GxEPD2_DRIVER_CLASS(/*CS=*/ PD8, /*DC=*/ PE15, /*RST=*/ PE14, /*BUSY=*/ PE13, /*CS2=*/ PD12)); // for GDEM1085T51 172 | GxEPD2_DISPLAY_CLASS display(GxEPD2_DRIVER_CLASS(/*CS=PA4*/ EPD_CS, /*DC=*/PA3, /*RST=*/PA2, /*BUSY=*/PA1)); // my proto board 173 | #undef MAX_DISPLAY_BUFFER_SIZE 174 | #undef MAX_HEIGHT 175 | #endif 176 | 177 | #if defined(ARDUINO_ARCH_RENESAS) 178 | #if defined(ARDUINO_UNOR4_MINIMA) || defined(ARDUINO_UNOR4_WIFI) 179 | #define MAX_DISPLAY_BUFFER_SIZE 16384ul // e.g. half of available RAM 180 | #if IS_GxEPD2_BW(GxEPD2_DISPLAY_CLASS) 181 | #define MAX_HEIGHT(EPD) (EPD::HEIGHT <= MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8) ? EPD::HEIGHT : MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8)) 182 | #elif IS_GxEPD2_3C(GxEPD2_DISPLAY_CLASS) || IS_GxEPD2_4C(GxEPD2_DISPLAY_CLASS) 183 | #define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE / 2) / (EPD::WIDTH / 8) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE / 2) / (EPD::WIDTH / 8)) 184 | #elif IS_GxEPD2_7C(GxEPD2_DISPLAY_CLASS) 185 | #define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2)) 186 | #endif 187 | // adapt the constructor parameters to your wiring 188 | GxEPD2_DISPLAY_CLASS display(GxEPD2_DRIVER_CLASS(/*CS=*/EPD_CS, /*DC=*/8, /*RST=*/9, /*BUSY=*/7)); 189 | #endif 190 | #endif 191 | 192 | #if defined(ARDUINO_ARCH_SAM) 193 | #define MAX_DISPLAY_BUFFER_SIZE 32768ul // e.g., up to 96k 194 | #if IS_GxEPD2_BW(GxEPD2_DISPLAY_CLASS) 195 | #define MAX_HEIGHT(EPD) (EPD::HEIGHT <= MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8) ? EPD::HEIGHT : MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8)) 196 | #elif IS_GxEPD2_3C(GxEPD2_DISPLAY_CLASS) || IS_GxEPD2_4C(GxEPD2_DISPLAY_CLASS) 197 | #define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE / 2) / (EPD::WIDTH / 8) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE / 2) / (EPD::WIDTH / 8)) 198 | #elif IS_GxEPD2_7C(GxEPD2_DISPLAY_CLASS) 199 | #define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2)) 200 | #endif 201 | // adapt the constructor parameters to your wiring 202 | GxEPD2_DISPLAY_CLASS display(GxEPD2_DRIVER_CLASS(/*CS=10*/ EPD_CS, /*DC=*/8, /*RST=*/9, /*BUSY=*/7)); 203 | #undef MAX_DISPLAY_BUFFER_SIZE 204 | #undef MAX_HEIGHT 205 | #endif 206 | 207 | #if defined(ARDUINO_ARCH_SAMD) 208 | #define MAX_DISPLAY_BUFFER_SIZE 15000ul // ~15k is a good compromise 209 | #if IS_GxEPD2_BW(GxEPD2_DISPLAY_CLASS) 210 | #define MAX_HEIGHT(EPD) (EPD::HEIGHT <= MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8) ? EPD::HEIGHT : MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8)) 211 | #elif IS_GxEPD2_3C(GxEPD2_DISPLAY_CLASS) || IS_GxEPD2_4C(GxEPD2_DISPLAY_CLASS) 212 | #define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE / 2) / (EPD::WIDTH / 8) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE / 2) / (EPD::WIDTH / 8)) 213 | #elif IS_GxEPD2_7C(GxEPD2_DISPLAY_CLASS) 214 | #define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2)) 215 | #endif 216 | #if defined(ARDUINO_SAMD_NANO_33_IOT) 217 | GxEPD2_DISPLAY_CLASS display(GxEPD2_DRIVER_CLASS(/*CS=10*/ EPD_CS, /*DC=*/8, /*RST=*/9, /*BUSY=*/7)); 218 | #else 219 | // adapt the constructor parameters to your wiring 220 | GxEPD2_DISPLAY_CLASS display(GxEPD2_DRIVER_CLASS(/*CS=4*/ 4, /*DC=*/7, /*RST=*/6, /*BUSY=*/5)); 221 | // GxEPD2_DISPLAY_CLASS display(GxEPD2_DRIVER_CLASS(/*CS=4*/ 4, /*DC=*/ 3, /*RST=*/ 2, /*BUSY=*/ 1)); // my Seed XIOA0 222 | // GxEPD2_DISPLAY_CLASS display(GxEPD2_DRIVER_CLASS(/*CS=4*/ 3, /*DC=*/ 2, /*RST=*/ 1, /*BUSY=*/ 0)); // my other Seed XIOA0 223 | #endif 224 | #undef MAX_DISPLAY_BUFFER_SIZE 225 | #undef MAX_HEIGHT 226 | #endif 227 | 228 | #if defined(ARDUINO_ARCH_RP2040) 229 | #define MAX_DISPLAY_BUFFER_SIZE 131072ul // e.g. half of available ram 230 | #if IS_GxEPD2_BW(GxEPD2_DISPLAY_CLASS) 231 | #define MAX_HEIGHT(EPD) (EPD::HEIGHT <= MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8) ? EPD::HEIGHT : MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8)) 232 | #elif IS_GxEPD2_3C(GxEPD2_DISPLAY_CLASS) || IS_GxEPD2_4C(GxEPD2_DISPLAY_CLASS) 233 | #define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE / 2) / (EPD::WIDTH / 8) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE / 2) / (EPD::WIDTH / 8)) 234 | #elif IS_GxEPD2_7C(GxEPD2_DISPLAY_CLASS) 235 | #define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2)) 236 | #endif 237 | #if defined(ARDUINO_NANO_RP2040_CONNECT) 238 | // adapt the constructor parameters to your wiring 239 | GxEPD2_DISPLAY_CLASS display(GxEPD2_DRIVER_CLASS(/*CS=*/EPD_CS, /*DC=*/8, /*RST=*/9, /*BUSY=*/7)); 240 | #endif 241 | #if defined(ARDUINO_RASPBERRY_PI_PICO) 242 | // adapt the constructor parameters to your wiring 243 | // GxEPD2_DISPLAY_CLASS display(GxEPD2_DRIVER_CLASS(/*CS=*/ 5, /*DC=*/ 8, /*RST=*/ 9, /*BUSY=*/ 7)); // my proto board 244 | // mapping of GoodDisplay DESPI-PICO. NOTE: uses alternate HW SPI pins! 245 | GxEPD2_DISPLAY_CLASS display(GxEPD2_DRIVER_CLASS(/*CS=*/3, /*DC=*/2, /*RST=*/1, /*BUSY=*/0)); // DESPI-PICO 246 | // GxEPD2_DISPLAY_CLASS display(GxEPD2_DRIVER_CLASS(/*CS=*/ 3, /*DC=*/ 2, /*RST=*/ 11, /*BUSY=*/ 10)); // DESPI-PICO modified 247 | // GxEPD2_DISPLAY_CLASS display(GxEPD2_DRIVER_CLASS(/*CS=*/ 9, /*DC=*/ 8, /*RST=*/ 12, /*BUSY=*/ 13)); // Waveshare Pico-ePaper-2.9 248 | #endif 249 | #if defined(ARDUINO_RASPBERRY_PI_PICO_W) 250 | GxEPD2_DISPLAY_CLASS display(GxEPD2_DRIVER_CLASS(/*CS=*/9, /*DC=*/8, /*RST=*/12, /*BUSY=*/13)); // Waveshare Pico-ePaper-2.9 251 | #endif 252 | #if defined(ARDUINO_ADAFRUIT_FEATHER_RP2040_THINKINK) 253 | // Adafruit Feather RP2040 ThinkInk used with package https://github.com/earlephilhower/arduino-pico 254 | GxEPD2_DISPLAY_CLASS display(GxEPD2_DRIVER_CLASS(/*CS=*/PIN_EPD_CS, /*DC=*/PIN_EPD_DC, /*RST=*/PIN_EPD_RESET, /*BUSY=*/PIN_EPD_BUSY)); 255 | #endif 256 | #undef MAX_DISPLAY_BUFFER_SIZE 257 | #undef MAX_HEIGHT 258 | #endif 259 | 260 | #endif 261 | 262 | #endif -------------------------------------------------------------------------------- /src/GxEPD2_selection_check.h: -------------------------------------------------------------------------------- 1 | // Display Library example for SPI e-paper panels from Dalian Good Display and boards from Waveshare. 2 | // Requires HW SPI and Adafruit_GFX. Caution: the e-paper panels require 3.3V supply AND data lines! 3 | // 4 | // Display Library based on Demo Example from Good Display: https://www.good-display.com/companyfile/32/ 5 | // 6 | // Author: Jean-Marc Zingg 7 | // 8 | // Version: see library.properties 9 | // 10 | // Library: https://github.com/ZinggJM/GxEPD2 11 | 12 | // Supporting Arduino Forum Topics (closed, read only): 13 | // Good Display ePaper for Arduino: https://forum.arduino.cc/t/good-display-epaper-for-arduino/419657 14 | // Waveshare e-paper displays with SPI: https://forum.arduino.cc/t/waveshare-e-paper-displays-with-spi/467865 15 | // 16 | // Add new topics in https://forum.arduino.cc/c/using-arduino/displays/23 for new questions and issues 17 | 18 | #define GxEPD2_102_IS_BW true 19 | #define GxEPD2_150_BN_IS_BW true 20 | #define GxEPD2_154_IS_BW true 21 | #define GxEPD2_154_D67_IS_BW true 22 | #define GxEPD2_154_T8_IS_BW true 23 | #define GxEPD2_154_M09_IS_BW true 24 | #define GxEPD2_154_M10_IS_BW true 25 | #define GxEPD2_154_GDEY0154D67_IS_BW true 26 | #define GxEPD2_213_IS_BW true 27 | #define GxEPD2_213_B72_IS_BW true 28 | #define GxEPD2_213_B73_IS_BW true 29 | #define GxEPD2_213_B74_IS_BW true 30 | #define GxEPD2_213_flex_IS_BW true 31 | #define GxEPD2_213_M21_IS_BW true 32 | #define GxEPD2_213_T5D_IS_BW true 33 | #define GxEPD2_213_BN_IS_BW true 34 | #define GxEPD2_213_GDEY0213B74_IS_BW true 35 | #define GxEPD2_260_IS_BW true 36 | #define GxEPD2_260_M01_IS_BW true 37 | #define GxEPD2_266_BN_IS_BW true 38 | #define GxEPD2_266_GDEY0266T90_IS_BW true 39 | #define GxEPD2_270_IS_BW true 40 | #define GxEPD2_270_GDEY027T91_IS_BW true 41 | #define GxEPD2_290_IS_BW true 42 | #define GxEPD2_290_T5_IS_BW true 43 | #define GxEPD2_290_T5D_IS_BW true 44 | #define GxEPD2_290_I6FD_IS_BW true 45 | #define GxEPD2_290_T94_IS_BW true 46 | #define GxEPD2_290_T94_V2_IS_BW true 47 | #define GxEPD2_290_BS_IS_BW true 48 | #define GxEPD2_290_M06_IS_BW true 49 | #define GxEPD2_290_GDEY029T94_IS_BW true 50 | #define GxEPD2_290_GDEY029T71H_IS_BW true 51 | #define GxEPD2_310_GDEQ031T10_IS_BW true 52 | #define GxEPD2_371_IS_BW true 53 | #define GxEPD2_370_TC1_IS_BW true 54 | #define GxEPD2_420_IS_BW true 55 | #define GxEPD2_420_M01_IS_BW true 56 | #define GxEPD2_420_GDEY042T81_IS_BW true 57 | #define GxEPD2_420_GYE042A87_IS_BW true 58 | #define GxEPD2_420_SE0420NQ04_IS_BW true 59 | #define GxEPD2_426_GDEQ0426T82_IS_BW true 60 | #define GxEPD2_579_GDEY0579T93_IS_BW true 61 | #define GxEPD2_583_IS_BW true 62 | #define GxEPD2_583_T8_IS_BW true 63 | #define GxEPD2_583_GDEQ0583T31_IS_BW true 64 | #define GxEPD2_750_IS_BW true 65 | #define GxEPD2_750_T7_IS_BW true 66 | #define GxEPD2_750_GDEY075T7_IS_BW true 67 | #define GxEPD2_1020_GDEM102T91_IS_BW true 68 | #define GxEPD2_1085_GDEM1085T51_IS_BW true 69 | #define GxEPD2_1160_T91_IS_BW true 70 | #define GxEPD2_1248_IS_BW true 71 | #define GxEPD2_1330_GDEM133T91_IS_BW true 72 | #define GxEPD2_it60_IS_BW true 73 | #define GxEPD2_it60_1448x1072_IS_BW true 74 | #define GxEPD2_it78_1872x1404_IS_BW true 75 | #define GxEPD2_it103_1872x1404_IS_BW true 76 | // 3-color e-papers 77 | #define GxEPD2_154c_IS_3C true 78 | #define GxEPD2_154_Z90c_IS_3C true 79 | #define GxEPD2_213c_IS_3C true 80 | #define GxEPD2_213_Z19c_IS_3C true 81 | #define GxEPD2_213_Z98c_IS_3C true 82 | #define GxEPD2_266c_IS_3C true 83 | #define GxEPD2_270c_IS_3C true 84 | #define GxEPD2_290c_IS_3C true 85 | #define GxEPD2_290_Z13c_IS_3C true 86 | #define GxEPD2_290_C90c_IS_3C true 87 | #define GxEPD2_420c_IS_3C true 88 | #define GxEPD2_420c_Z21_IS_3C true 89 | #define GxEPD2_420c_GDEY042Z98_IS_3C true 90 | #define GxEPD2_579c_GDEY0579Z93_IS_3C true 91 | #define GxEPD2_583c_IS_3C true 92 | #define GxEPD2_583c_Z83_IS_3C true 93 | #define GxEPD2_583c_GDEQ0583Z31_IS_3C true 94 | #define GxEPD2_750c_IS_3C true 95 | #define GxEPD2_750c_Z08_IS_3C true 96 | #define GxEPD2_750c_Z90_IS_3C true 97 | #define GxEPD2_1160c_GDEY116Z91_IS_3C true 98 | #define GxEPD2_1248c_IS_3C true 99 | #define GxEPD2_1330c_GDEM133Z91_IS_3C true 100 | // 4-color e-paper 101 | #define GxEPD2_213c_GDEY0213F51_IS_4C true 102 | #define GxEPD2_266c_GDEY0266F51H_IS_4C true 103 | #define GxEPD2_290c_GDEY029F51H_IS_4C true 104 | #define GxEPD2_300c_IS_4C true 105 | #define GxEPD2_420c_GDEY0420F51_IS_4C true 106 | #define GxEPD2_437c_IS_4C true 107 | #define GxEPD2_0579c_GDEY0579F51_IS_4C true 108 | #define GxEPD2_1160c_GDEY116F51_IS_4C true 109 | // 7-color e-paper 110 | #define GxEPD2_565c_IS_7C true 111 | #define GxEPD2_565c_GDEP0565D90_IS_7C true 112 | #define GxEPD2_730c_GDEY073D46_IS_7C true 113 | #define GxEPD2_730c_ACeP_730_IS_7C true 114 | #define GxEPD2_730c_GDEP073E01_IS_7C true 115 | 116 | #if defined(GxEPD2_DISPLAY_CLASS) && defined(GxEPD2_DRIVER_CLASS) 117 | #define IS_GxEPD2_DRIVER(c, x) (c##x) 118 | #define IS_GxEPD2_DRIVER_BW(x) IS_GxEPD2_DRIVER(x, _IS_BW) 119 | #define IS_GxEPD2_DRIVER_3C(x) IS_GxEPD2_DRIVER(x, _IS_3C) 120 | #define IS_GxEPD2_DRIVER_4C(x) IS_GxEPD2_DRIVER(x, _IS_4C) 121 | #define IS_GxEPD2_DRIVER_7C(x) IS_GxEPD2_DRIVER(x, _IS_7C) 122 | #if IS_GxEPD2_BW(GxEPD2_DISPLAY_CLASS) && IS_GxEPD2_DRIVER_3C(GxEPD2_DRIVER_CLASS) 123 | #error "GxEPD2_BW used with 3-color driver class" 124 | #endif 125 | #if IS_GxEPD2_BW(GxEPD2_DISPLAY_CLASS) && IS_GxEPD2_DRIVER_4C(GxEPD2_DRIVER_CLASS) 126 | #error "GxEPD2_BW used with 4-color driver class" 127 | #endif 128 | #if IS_GxEPD2_BW(GxEPD2_DISPLAY_CLASS) && IS_GxEPD2_DRIVER_7C(GxEPD2_DRIVER_CLASS) 129 | #error "GxEPD2_BW used with 7-color driver class" 130 | #endif 131 | #if IS_GxEPD2_3C(GxEPD2_DISPLAY_CLASS) && IS_GxEPD2_DRIVER_BW(GxEPD2_DRIVER_CLASS) 132 | #error "GxEPD2_3C used with b/w driver class" 133 | #endif 134 | #if IS_GxEPD2_3C(GxEPD2_DISPLAY_CLASS) && IS_GxEPD2_DRIVER_4C(GxEPD2_DRIVER_CLASS) 135 | #error "GxEPD2_3C used with 4-color driver class" 136 | #endif 137 | #if IS_GxEPD2_3C(GxEPD2_DISPLAY_CLASS) && IS_GxEPD2_DRIVER_7C(GxEPD2_DRIVER_CLASS) 138 | #error "GxEPD2_3C used with 7-color driver class" 139 | #endif 140 | #if IS_GxEPD2_4C(GxEPD2_DISPLAY_CLASS) && IS_GxEPD2_DRIVER_BW(GxEPD2_DRIVER_CLASS) 141 | #error "GxEPD2_4C used with b/w driver class" 142 | #endif 143 | #if IS_GxEPD2_4C(GxEPD2_DISPLAY_CLASS) && IS_GxEPD2_DRIVER_3C(GxEPD2_DRIVER_CLASS) 144 | #error "GxEPD2_4C used with 3-color driver class" 145 | #endif 146 | #if IS_GxEPD2_4C(GxEPD2_DISPLAY_CLASS) && IS_GxEPD2_DRIVER_7C(GxEPD2_DRIVER_CLASS) 147 | #error "GxEPD2_4C used with 7-color driver class" 148 | #endif 149 | #if IS_GxEPD2_7C(GxEPD2_DISPLAY_CLASS) && !IS_GxEPD2_DRIVER_7C(GxEPD2_DRIVER_CLASS) 150 | #error "GxEPD2_7C used with less colors driver class" 151 | #endif 152 | #if !IS_GxEPD2_DRIVER_BW(GxEPD2_DRIVER_CLASS) && !IS_GxEPD2_DRIVER_3C(GxEPD2_DRIVER_CLASS) && !IS_GxEPD2_DRIVER_4C(GxEPD2_DRIVER_CLASS) && !IS_GxEPD2_DRIVER_7C(GxEPD2_DRIVER_CLASS) 153 | #error "neither BW nor 3C nor 4C nor 7C kind defined for driver class (error in GxEPD2_selection_check.h)" 154 | #endif 155 | 156 | #endif 157 | -------------------------------------------------------------------------------- /src/anniversary.h: -------------------------------------------------------------------------------- 1 | #ifndef ANNIVERSARY_H 2 | #define ANNIVERSARY_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include "timer.h" 8 | #include "gfx_utils.h" 9 | #include "icons.h" 10 | #include "icon.h" 11 | #include "menu.h" 12 | #include "debug.h" 13 | #include "button.h" 14 | #include 15 | 16 | class Anniversary 17 | { 18 | private: 19 | DISPLAY_CLASS &display; 20 | 21 | Menu buttons = Menu(display, new MenuItem[1]{MenuItem("Weiter")}, 1); 22 | 23 | int page = 0; 24 | const uint16_t numPages = 17; 25 | unsigned long lastRedrawTime = 0; 26 | 27 | void drawPage(uint16_t x, uint16_t y, uint16_t w, uint16_t h); 28 | 29 | public: 30 | Anniversary(DISPLAY_CLASS &display); 31 | ~Anniversary(); 32 | void draw(); 33 | void loop(); 34 | }; 35 | 36 | #endif 37 | -------------------------------------------------------------------------------- /src/button.cpp: -------------------------------------------------------------------------------- 1 | #include "button.h" 2 | 3 | static const unsigned long DEBOUNCE_DELAY = 1000; // ms 4 | 5 | Button *Button::instance = nullptr; 6 | bool Button::instanceExists = false; 7 | 8 | Button::Button(int pin) : pin(pin) 9 | { 10 | if (instanceExists) 11 | { 12 | Serial.println("ERROR: Only one Button instance allowed!"); 13 | return; 14 | } 15 | 16 | pinMode(pin, INPUT_PULLUP); 17 | attachInterrupt(pin, buttonInterruptHandler, FALLING); 18 | instance = this; 19 | instanceExists = true; 20 | } 21 | 22 | Button::~Button() 23 | { 24 | if (instance == this) 25 | { 26 | instance = nullptr; 27 | instanceExists = false; 28 | detachInterrupt(pin); 29 | } 30 | } 31 | 32 | void IRAM_ATTR Button::buttonInterruptHandler() 33 | { 34 | if (!instance) 35 | return; // Safety check 36 | 37 | unsigned long currentTime = millis(); 38 | 39 | if ((currentTime - instance->lastPressTime) > DEBOUNCE_DELAY) 40 | { 41 | instance->pressed = true; 42 | instance->lastPressTime = currentTime; 43 | } 44 | } 45 | 46 | bool Button::checkAndClearButtonPress() 47 | { 48 | if (!instanceExists) 49 | return false; // Safety check 50 | 51 | if (pressed) 52 | { 53 | Serial.println("Button pressed"); 54 | pressed = false; 55 | return true; 56 | } 57 | return false; 58 | } -------------------------------------------------------------------------------- /src/button.h: -------------------------------------------------------------------------------- 1 | #ifndef BUTTON_H 2 | #define BUTTON_H 3 | 4 | #include 5 | 6 | class Button 7 | { 8 | private: 9 | int pin; 10 | static bool instanceExists; 11 | 12 | public: 13 | volatile bool pressed = false; 14 | volatile unsigned long lastPressTime = 0; 15 | 16 | static Button *instance; 17 | 18 | Button(int pin); 19 | ~Button(); 20 | 21 | static void IRAM_ATTR buttonInterruptHandler(); 22 | bool checkAndClearButtonPress(); 23 | }; 24 | #endif -------------------------------------------------------------------------------- /src/checkbox.cpp: -------------------------------------------------------------------------------- 1 | #include "checkbox.h" 2 | #include "preferences_manager.h" 3 | 4 | extern Preferences preferences; 5 | 6 | #define NOVALUE 0 7 | #define TRUEVALUE -1 8 | #define FALSEVALUE 1 9 | 10 | Checkbox::Checkbox(Icon *icon, const char *name, const char *key, bool defaultValue) : icon(icon), name(name), key(key), defaultValue(defaultValue) 11 | { 12 | load(); 13 | } 14 | 15 | Checkbox::~Checkbox() 16 | { 17 | save(); 18 | } 19 | 20 | Icon *Checkbox::getIcon() 21 | { 22 | return icon; 23 | } 24 | 25 | const char *Checkbox::getName() 26 | { 27 | return name; 28 | } 29 | 30 | bool Checkbox::isChecked() 31 | { 32 | return checked; 33 | } 34 | 35 | void Checkbox::toggle() 36 | { 37 | checked = !checked; 38 | } 39 | 40 | void Checkbox::load() 41 | { 42 | checked = pref_getCheckbox(key, defaultValue); 43 | 44 | Serial.printf("Checkbox::load: key=%s, value=%s\n", key, checked ? "true" : "false"); 45 | } 46 | 47 | void Checkbox::save() 48 | { 49 | pref_putCheckbox(key, checked); 50 | 51 | Serial.printf("Checkbox::save: key=%s, value=%s\n", key, checked ? "true" : "false"); 52 | } 53 | 54 | void Checkbox::draw( 55 | DISPLAY_CLASS &display, 56 | uint16_t x, 57 | uint16_t y, 58 | uint16_t w, 59 | uint16_t h, 60 | bool selected) 61 | { 62 | const uint16_t padding = 12; 63 | 64 | display.fillRect(x, y, w, h, GxEPD_WHITE); 65 | display.drawRoundRect(x, y, w, h, 10, GxEPD_BLACK); 66 | 67 | if (selected) 68 | { 69 | drawPatternInRoundedArea(display, x, y, w, h, 10, Pattern::SparseDots); 70 | } 71 | 72 | ScaledIcon checkmark = icon_checkmark.scaled(48); 73 | 74 | const uint16_t iconSize = 64; 75 | if (icon) 76 | { 77 | ScaledIcon scaledIcon = icon->scaled(iconSize); 78 | display.drawBitmap(x + padding, y + padding, scaledIcon.data, scaledIcon.size, scaledIcon.size, GxEPD_BLACK); 79 | } 80 | 81 | Bounds bounds = getBounds(display, name, &MAIN_FONT); 82 | const uint16_t textX = x + padding + iconSize + padding; 83 | const uint16_t textY = y + h / 2 + bounds.h / 2; 84 | 85 | drawText(display, name, textX, textY, &MAIN_FONT, GxEPD_BLACK); 86 | 87 | display.drawRoundRect(x + w - padding - 64, y + h / 2 - 64 / 2, 64, 64, 10, GxEPD_BLACK); 88 | 89 | if (checked) 90 | { 91 | display.drawBitmap(x + w - padding - 64 / 2 - checkmark.size / 2, y + h / 2 - checkmark.size / 2, checkmark.data, checkmark.size, checkmark.size, GxEPD_BLACK); 92 | } 93 | } -------------------------------------------------------------------------------- /src/checkbox.h: -------------------------------------------------------------------------------- 1 | #ifndef CHECKBOX_H 2 | #define CHECKBOX_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "icons.h" 9 | #include "icon.h" 10 | #include "gfx_utils.h" 11 | #include "debug.h" 12 | #include "defs.h" 13 | 14 | class Checkbox 15 | { 16 | private: 17 | Icon *icon; 18 | const char *name; 19 | const char *key; 20 | bool checked; 21 | bool defaultValue; 22 | 23 | Preferences preferences; 24 | 25 | public: 26 | Checkbox(Icon *icon, const char *name, const char *key, bool defaultValue = false); 27 | ~Checkbox(); 28 | Icon *getIcon(); 29 | const char *getName(); 30 | bool isChecked(); 31 | void toggle(); 32 | 33 | void load(); 34 | void save(); 35 | 36 | void draw( 37 | DISPLAY_CLASS &display, 38 | uint16_t x, 39 | uint16_t y, 40 | uint16_t w, 41 | uint16_t h, 42 | bool selected); 43 | }; 44 | 45 | #endif -------------------------------------------------------------------------------- /src/debug.h: -------------------------------------------------------------------------------- 1 | #ifndef DEBUG_H 2 | 3 | #define DEBUG_H 4 | 5 | // #define DEBUG 6 | 7 | // #define ICON_SCALING_TEST 1 8 | // #define PATTERN_TEST 1 9 | // #define IMAGE_CYCLE_TEST 1 10 | // #define CHECKBOX_TEST 1 11 | // #define STRINGS_TEST 1 12 | 13 | #endif -------------------------------------------------------------------------------- /src/defs.h: -------------------------------------------------------------------------------- 1 | #ifndef DEFS_H 2 | #define DEFS_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include "fonts/FunnelDisplay_Bold24pt7b.h" 9 | #include "fonts/FunnelDisplay_Bold18pt7b.h" 10 | #include "fonts/FunnelDisplay_Regular14pt7b.h" 11 | #include "fonts/HelvetiPixel24pt7b.h" 12 | #include "fonts/HelvetiPixel16pt7b.h" 13 | #include "fonts/FunnelDisplay_Bold48pt7b.h" 14 | #include "fonts/FunnelDisplay_Bold32pt7b.h" 15 | #include "fonts/FunnelDisplay_Bold60pt7b.h" 16 | 17 | #define ANNIVERSARY_MODE false 18 | 19 | #define TITLE_FONT FunnelDisplay_Bold32pt7b 20 | #define MAIN_FONT FunnelDisplay_Bold24pt7b 21 | #define TEXT_FONT FunnelDisplay_Regular14pt7b 22 | #define SECONDARY_FONT FunnelDisplay_Bold18pt7b 23 | #define SUB_FONT HelvetiPixel24pt7b 24 | #define SMALL_FONT HelvetiPixel16pt7b 25 | #define SEMI_LARGE_FONT FunnelDisplay_Bold48pt7b 26 | #define LARGE_FONT FunnelDisplay_Bold60pt7b 27 | 28 | #define REDRAW_INTERVAL_SLOW 1000 29 | 30 | #define DISPLAY_CLASS GxEPD2_BW 31 | 32 | #endif 33 | -------------------------------------------------------------------------------- /src/fonts/FunnelDisplay_Regular14pt7b.h: -------------------------------------------------------------------------------- 1 | const uint8_t FunnelDisplay_Regular14pt7bBitmaps[] PROGMEM = { 2 | 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0xFF, 0x80, 0xC3, 0xC3, 0xC3, 3 | 0xC3, 0xC3, 0xC3, 0xC3, 0x07, 0x18, 0x0C, 0x30, 0x18, 0x60, 0x30, 0xC0, 4 | 0x63, 0x01, 0xC6, 0x0F, 0xFF, 0x9F, 0xFF, 0x0C, 0x30, 0x18, 0xC0, 0x21, 5 | 0x80, 0xC3, 0x0F, 0xFF, 0xDF, 0xFF, 0x86, 0x30, 0x0C, 0x60, 0x30, 0xC0, 6 | 0x61, 0x80, 0xC3, 0x00, 0x03, 0x00, 0x0C, 0x01, 0xFC, 0x0F, 0xFC, 0x70, 7 | 0x79, 0x80, 0xE6, 0x01, 0xD8, 0x00, 0x70, 0x00, 0xFC, 0x01, 0xFE, 0x00, 8 | 0xFE, 0x00, 0x38, 0x00, 0x7E, 0x01, 0xF8, 0x07, 0xF0, 0x1D, 0xFB, 0xE3, 9 | 0xFF, 0x01, 0xF0, 0x03, 0x00, 0x0C, 0x00, 0x1C, 0x01, 0x83, 0xF8, 0x18, 10 | 0x18, 0xC0, 0xC1, 0x87, 0x0C, 0x0C, 0x18, 0x60, 0x60, 0xC6, 0x03, 0x06, 11 | 0x30, 0x1C, 0x73, 0x00, 0x67, 0x38, 0x01, 0xF1, 0x8F, 0x00, 0x18, 0xFC, 12 | 0x00, 0xCC, 0x30, 0x0C, 0x61, 0x80, 0x63, 0x0C, 0x06, 0x18, 0x60, 0x70, 13 | 0xC3, 0x03, 0x06, 0x18, 0x30, 0x19, 0xC1, 0x80, 0xFC, 0x03, 0xE0, 0x0F, 14 | 0xF0, 0x0F, 0xF8, 0x0E, 0x00, 0x07, 0x00, 0x03, 0x80, 0x61, 0xC0, 0x30, 15 | 0x60, 0x18, 0x1F, 0xFF, 0x8F, 0xFF, 0xDF, 0x03, 0x0E, 0x01, 0x8E, 0x00, 16 | 0xC7, 0x00, 0x63, 0x80, 0x31, 0xC0, 0x18, 0x78, 0x0C, 0x1F, 0xFE, 0x07, 17 | 0xFF, 0x00, 0xFF, 0xFC, 0x1C, 0x63, 0x8C, 0x71, 0x86, 0x38, 0xE3, 0x8C, 18 | 0x30, 0xC3, 0x0E, 0x38, 0xE1, 0x87, 0x1C, 0x30, 0xE1, 0xC0, 0xC3, 0x87, 19 | 0x0C, 0x38, 0xE1, 0x86, 0x1C, 0x71, 0xC7, 0x1C, 0x71, 0xC6, 0x18, 0x63, 20 | 0x8C, 0x71, 0x8E, 0x00, 0x06, 0x00, 0x60, 0x06, 0x04, 0x63, 0xFF, 0xF3, 21 | 0xFC, 0x0F, 0x00, 0xF8, 0x19, 0x83, 0x9C, 0x30, 0xC0, 0x06, 0x00, 0x60, 22 | 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0xFF, 0xFF, 0xFF, 0x06, 0x00, 0x60, 23 | 0x06, 0x00, 0x60, 0x06, 0x00, 0x77, 0x73, 0x3E, 0xC0, 0xFF, 0xF0, 0x7F, 24 | 0x70, 0x01, 0xC0, 0x60, 0x38, 0x0C, 0x03, 0x01, 0xC0, 0x60, 0x18, 0x0E, 25 | 0x03, 0x00, 0xC0, 0x70, 0x18, 0x0E, 0x03, 0x00, 0xC0, 0x70, 0x18, 0x0E, 26 | 0x00, 0x07, 0x80, 0x7F, 0x83, 0xCF, 0x1C, 0x0C, 0x70, 0x39, 0x80, 0x6E, 27 | 0x01, 0xB8, 0x07, 0xE0, 0x1F, 0x80, 0x7E, 0x01, 0xF8, 0x07, 0xE0, 0x1B, 28 | 0x80, 0x66, 0x03, 0x9C, 0x0E, 0x38, 0x70, 0xFF, 0x80, 0xFC, 0x00, 0x03, 29 | 0x00, 0xF0, 0x1F, 0x07, 0xF0, 0xF7, 0x0C, 0x70, 0x87, 0x00, 0x70, 0x07, 30 | 0x00, 0x70, 0x07, 0x00, 0x70, 0x07, 0x00, 0x70, 0x07, 0x00, 0x70, 0x07, 31 | 0x07, 0xFF, 0x7F, 0xF0, 0x07, 0x80, 0xFF, 0x0F, 0x3C, 0xE0, 0x76, 0x03, 32 | 0xF0, 0x0C, 0x00, 0xE0, 0x07, 0x00, 0x70, 0x07, 0x00, 0x70, 0x07, 0x00, 33 | 0x70, 0x07, 0x00, 0x70, 0x07, 0x00, 0x70, 0x07, 0xFF, 0xFF, 0xFE, 0x07, 34 | 0x81, 0xFF, 0x1F, 0x3C, 0xE0, 0x7E, 0x03, 0x80, 0x1C, 0x00, 0xE0, 0x06, 35 | 0x0F, 0xE0, 0x7F, 0x00, 0x3C, 0x00, 0x70, 0x01, 0x80, 0x0F, 0x80, 0x7C, 36 | 0x07, 0x70, 0x7B, 0xFF, 0x87, 0xF8, 0x00, 0x38, 0x00, 0xF0, 0x01, 0xE0, 37 | 0x07, 0xC0, 0x1F, 0x80, 0x77, 0x01, 0xCE, 0x03, 0x1C, 0x0C, 0x38, 0x38, 38 | 0x70, 0xE0, 0xE3, 0x81, 0xC7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x1C, 39 | 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, 0x3F, 0xF1, 0xFF, 0x8F, 0xFC, 0x60, 40 | 0x07, 0x00, 0x38, 0x01, 0xC0, 0x0D, 0xF8, 0x7F, 0xF3, 0xC3, 0x98, 0x0E, 41 | 0x00, 0x30, 0x01, 0x80, 0x0F, 0x80, 0x7C, 0x07, 0x70, 0x73, 0xFF, 0x87, 42 | 0xF0, 0x07, 0xC0, 0x7F, 0xC3, 0xC7, 0x8E, 0x0E, 0x70, 0x1D, 0x80, 0x06, 43 | 0x00, 0x39, 0xF0, 0xEF, 0xF3, 0xE1, 0xEF, 0x03, 0xB8, 0x07, 0xE0, 0x1F, 44 | 0x80, 0x76, 0x01, 0xDC, 0x06, 0x38, 0x38, 0xFF, 0xC0, 0xFE, 0x00, 0xFF, 45 | 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x06, 0x00, 0x38, 0x01, 0xC0, 0x07, 0x00, 46 | 0x38, 0x00, 0xE0, 0x07, 0x00, 0x1C, 0x00, 0xE0, 0x03, 0x80, 0x1C, 0x00, 47 | 0x60, 0x03, 0x80, 0x0C, 0x00, 0x70, 0x03, 0x80, 0x00, 0x07, 0x80, 0xFF, 48 | 0x87, 0x8F, 0x1C, 0x0E, 0x60, 0x39, 0x80, 0x66, 0x03, 0x9C, 0x0C, 0x3F, 49 | 0xE0, 0x7F, 0x83, 0x8F, 0x1C, 0x0E, 0xE0, 0x1B, 0x80, 0x7E, 0x01, 0xF8, 50 | 0x06, 0x70, 0x78, 0xFF, 0xC1, 0xFE, 0x00, 0x0F, 0x01, 0xFE, 0x1F, 0x78, 51 | 0xE0, 0xEE, 0x03, 0xF0, 0x1F, 0x00, 0x7C, 0x07, 0xE0, 0x3B, 0x83, 0xDF, 52 | 0xF6, 0x3F, 0x30, 0x01, 0x80, 0x0F, 0x80, 0xFC, 0x06, 0x70, 0x71, 0xFF, 53 | 0x07, 0xF0, 0x7F, 0x70, 0x00, 0x00, 0x7F, 0x70, 0x7F, 0x70, 0x00, 0x00, 54 | 0x77, 0x73, 0x3E, 0xC0, 0x00, 0x30, 0x1F, 0x07, 0xC1, 0xF0, 0xF8, 0x0E, 55 | 0x00, 0xF0, 0x07, 0xE0, 0x0F, 0x80, 0x3E, 0x00, 0xF0, 0x01, 0xFF, 0xFF, 56 | 0xFF, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xF0, 0xC0, 0x0F, 0x80, 57 | 0x3E, 0x00, 0xF8, 0x03, 0xE0, 0x07, 0x00, 0xF0, 0x7C, 0x1F, 0x07, 0xC0, 58 | 0xE0, 0x08, 0x00, 0x1F, 0x07, 0xFC, 0xF9, 0xEE, 0x06, 0xC0, 0x70, 0x07, 59 | 0x00, 0x60, 0x0E, 0x03, 0xC0, 0x78, 0x0F, 0x00, 0xC0, 0x0C, 0x00, 0x00, 60 | 0x00, 0x00, 0x00, 0x1C, 0x01, 0xC0, 0x1C, 0x00, 0x00, 0x3F, 0x00, 0x03, 61 | 0xFF, 0xE0, 0x07, 0xC1, 0xF0, 0x1E, 0x00, 0x38, 0x18, 0x00, 0x1C, 0x30, 62 | 0x38, 0x0E, 0x70, 0xFD, 0x86, 0x61, 0xC7, 0x87, 0x61, 0x83, 0x83, 0xE3, 63 | 0x81, 0x83, 0xE3, 0x81, 0x83, 0xE3, 0x81, 0x83, 0xE3, 0x81, 0x86, 0x61, 64 | 0x81, 0x86, 0x61, 0xC3, 0x8C, 0x60, 0xFD, 0xF8, 0x30, 0x78, 0xF0, 0x38, 65 | 0x00, 0x00, 0x1C, 0x00, 0x00, 0x0F, 0x80, 0x00, 0x03, 0xFF, 0xE0, 0x00, 66 | 0x7F, 0xE0, 0x00, 0xE0, 0x00, 0x1C, 0x00, 0x07, 0xC0, 0x00, 0xD8, 0x00, 67 | 0x3B, 0x80, 0x06, 0x30, 0x01, 0xC7, 0x00, 0x38, 0xE0, 0x06, 0x0C, 0x01, 68 | 0xC1, 0xC0, 0x30, 0x18, 0x0F, 0xFF, 0x81, 0xFF, 0xF0, 0x70, 0x07, 0x0E, 69 | 0x00, 0xE1, 0x80, 0x0C, 0x70, 0x01, 0xCC, 0x00, 0x1B, 0x80, 0x03, 0x80, 70 | 0xFF, 0xC0, 0xFF, 0xF8, 0xFF, 0xFC, 0xE0, 0x1C, 0xE0, 0x0E, 0xE0, 0x0E, 71 | 0xE0, 0x0C, 0xE0, 0x1C, 0xFF, 0xF8, 0xFF, 0xF8, 0xE0, 0x3C, 0xE0, 0x0E, 72 | 0xE0, 0x06, 0xE0, 0x07, 0xE0, 0x06, 0xE0, 0x0E, 0xE0, 0x1E, 0xFF, 0xFC, 73 | 0xFF, 0xF0, 0x01, 0xF0, 0x07, 0xFE, 0x07, 0xFF, 0xC7, 0x80, 0xE7, 0x80, 74 | 0x3B, 0x80, 0x0D, 0x80, 0x01, 0xC0, 0x00, 0xE0, 0x00, 0x70, 0x00, 0x38, 75 | 0x00, 0x1C, 0x00, 0x0E, 0x00, 0x03, 0x80, 0x0D, 0xC0, 0x0E, 0x70, 0x07, 76 | 0x3E, 0x0F, 0x07, 0xFF, 0x01, 0xFE, 0x00, 0xFF, 0x00, 0x7F, 0xF8, 0x3F, 77 | 0xFF, 0x1C, 0x03, 0xCE, 0x00, 0xE7, 0x00, 0x3B, 0x80, 0x1D, 0xC0, 0x06, 78 | 0xE0, 0x03, 0xF0, 0x01, 0xF8, 0x00, 0xFC, 0x00, 0x7E, 0x00, 0x37, 0x00, 79 | 0x3B, 0x80, 0x1D, 0xC0, 0x3C, 0xE0, 0x7C, 0x7F, 0xFC, 0x3F, 0xF8, 0x00, 80 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x0E, 0x00, 0x70, 0x03, 0x80, 0x1C, 81 | 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x0E, 0x00, 0x70, 0x03, 0x80, 82 | 0x1C, 0x00, 0xE0, 0x07, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 83 | 0xC0, 0x0E, 0x00, 0x70, 0x03, 0x80, 0x1C, 0x00, 0xE0, 0x07, 0xFF, 0xBF, 84 | 0xFD, 0xC0, 0x0E, 0x00, 0x70, 0x03, 0x80, 0x1C, 0x00, 0xE0, 0x07, 0x00, 85 | 0x38, 0x00, 0x01, 0xF0, 0x01, 0xFF, 0x81, 0xFF, 0xF0, 0xF0, 0x1E, 0x38, 86 | 0x03, 0x9C, 0x00, 0x66, 0x00, 0x07, 0x80, 0x00, 0xE0, 0x00, 0x38, 0x00, 87 | 0x0E, 0x07, 0xFB, 0x81, 0xFF, 0xE0, 0x01, 0xDC, 0x00, 0x77, 0x00, 0x1C, 88 | 0xE0, 0x0F, 0x1E, 0x0F, 0xC3, 0xFF, 0x70, 0x7F, 0x9C, 0xE0, 0x03, 0xE0, 89 | 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xE0, 90 | 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x03, 0xE0, 0x03, 0xE0, 91 | 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xFF, 92 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x07, 0x07, 0x07, 0x07, 0x07, 93 | 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 94 | 0xFF, 0xFE, 0xE0, 0x0F, 0xE0, 0x1C, 0xE0, 0x38, 0xE0, 0x70, 0xE0, 0xE0, 95 | 0xE1, 0xC0, 0xE3, 0x80, 0xE7, 0x00, 0xEE, 0x00, 0xFF, 0x00, 0xFB, 0x80, 96 | 0xE3, 0xC0, 0xE1, 0xC0, 0xE0, 0xE0, 0xE0, 0x70, 0xE0, 0x38, 0xE0, 0x1C, 97 | 0xE0, 0x1E, 0xE0, 0x0F, 0xE0, 0x07, 0x00, 0x38, 0x01, 0xC0, 0x0E, 0x00, 98 | 0x70, 0x03, 0x80, 0x1C, 0x00, 0xE0, 0x07, 0x00, 0x38, 0x01, 0xC0, 0x0E, 99 | 0x00, 0x70, 0x03, 0x80, 0x1C, 0x00, 0xE0, 0x07, 0xFF, 0xFF, 0xFE, 0xF0, 100 | 0x00, 0x7F, 0xC0, 0x03, 0xFE, 0x00, 0x3F, 0xF0, 0x01, 0xFF, 0xC0, 0x1F, 101 | 0xF6, 0x00, 0xDF, 0xB8, 0x0E, 0xFD, 0xC0, 0x67, 0xE7, 0x03, 0x3F, 0x38, 102 | 0x39, 0xF8, 0xC1, 0x8F, 0xC7, 0x1C, 0x7E, 0x18, 0xC3, 0xF0, 0xEE, 0x1F, 103 | 0x87, 0x60, 0xFC, 0x1F, 0x07, 0xE0, 0xF8, 0x3F, 0x03, 0x81, 0xF8, 0x1C, 104 | 0x0E, 0xE0, 0x01, 0xF8, 0x00, 0xFE, 0x00, 0x7F, 0x00, 0x3F, 0xC0, 0x1F, 105 | 0x70, 0x0F, 0x9C, 0x07, 0xCE, 0x03, 0xE3, 0x81, 0xF0, 0xE0, 0xF8, 0x38, 106 | 0x7C, 0x1C, 0x3E, 0x07, 0x1F, 0x01, 0xCF, 0x80, 0x77, 0xC0, 0x3B, 0xE0, 107 | 0x0F, 0xF0, 0x03, 0xF8, 0x00, 0xE0, 0x01, 0xF0, 0x01, 0xFF, 0xC0, 0x7F, 108 | 0xFC, 0x1E, 0x03, 0xC7, 0x80, 0x3C, 0xE0, 0x03, 0x98, 0x00, 0x37, 0x00, 109 | 0x07, 0xE0, 0x00, 0xFC, 0x00, 0x1F, 0x80, 0x03, 0xF0, 0x00, 0x76, 0x00, 110 | 0x0C, 0xE0, 0x03, 0x9C, 0x00, 0x71, 0xC0, 0x1C, 0x1E, 0x0F, 0x01, 0xFF, 111 | 0xC0, 0x1F, 0xF0, 0x00, 0xFF, 0x81, 0xFF, 0xF3, 0xFF, 0xF7, 0x00, 0xEE, 112 | 0x00, 0xFC, 0x01, 0xF8, 0x03, 0xF0, 0x07, 0xE0, 0x0F, 0xC0, 0x7B, 0xFF, 113 | 0xE7, 0xFF, 0x0E, 0x00, 0x1C, 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, 0x01, 114 | 0xC0, 0x03, 0x80, 0x00, 0x01, 0xF0, 0x01, 0xFF, 0xC0, 0x7C, 0xFC, 0x1E, 115 | 0x03, 0xC7, 0x80, 0x3C, 0xE0, 0x03, 0x98, 0x00, 0x37, 0x00, 0x07, 0xE0, 116 | 0x00, 0xFC, 0x00, 0x1F, 0x80, 0x03, 0xF0, 0x00, 0x76, 0x00, 0x0C, 0xE0, 117 | 0x03, 0x9C, 0x00, 0x71, 0xC0, 0x1C, 0x1E, 0x0F, 0x01, 0xFF, 0xC0, 0x0F, 118 | 0xE0, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x01, 0xFE, 0x00, 0x1F, 0xC0, 0xFF, 119 | 0xC1, 0xFF, 0xF3, 0xFF, 0xF7, 0x00, 0x7E, 0x00, 0xFC, 0x00, 0xF8, 0x01, 120 | 0xF0, 0x07, 0xE0, 0x1D, 0xFF, 0xF3, 0xFF, 0xFF, 0x00, 0x3E, 0x00, 0x7C, 121 | 0x00, 0xF8, 0x01, 0xF0, 0x03, 0xE0, 0x07, 0xC0, 0x0F, 0x80, 0x18, 0x07, 122 | 0xC0, 0x3F, 0xE0, 0xF1, 0xF3, 0x80, 0xE7, 0x00, 0xEC, 0x01, 0xDC, 0x00, 123 | 0x3C, 0x00, 0x3F, 0x80, 0x3F, 0xF0, 0x07, 0xF0, 0x00, 0xF0, 0x00, 0xFC, 124 | 0x01, 0xF8, 0x03, 0xF8, 0x07, 0x78, 0x1C, 0x7F, 0xF8, 0x3F, 0xC0, 0xFF, 125 | 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x1C, 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, 126 | 0x01, 0xC0, 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x38, 0x00, 127 | 0x70, 0x00, 0xE0, 0x01, 0xC0, 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0xE0, 128 | 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 129 | 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 130 | 0x07, 0xE0, 0x07, 0xE0, 0x06, 0x70, 0x0E, 0x78, 0x3E, 0x3F, 0xFC, 0x0F, 131 | 0xF0, 0xE0, 0x00, 0xEE, 0x00, 0x19, 0xC0, 0x07, 0x18, 0x00, 0xE3, 0x80, 132 | 0x38, 0x70, 0x07, 0x07, 0x00, 0xC0, 0xE0, 0x38, 0x0C, 0x06, 0x01, 0xC1, 133 | 0xC0, 0x38, 0x38, 0x03, 0x86, 0x00, 0x71, 0xC0, 0x06, 0x30, 0x00, 0xEE, 134 | 0x00, 0x1D, 0xC0, 0x01, 0xF0, 0x00, 0x3E, 0x00, 0x03, 0x80, 0x00, 0xC0, 135 | 0x1C, 0x01, 0xF0, 0x0E, 0x01, 0xF8, 0x07, 0x00, 0xEC, 0x07, 0xC0, 0x67, 136 | 0x03, 0x60, 0x73, 0x81, 0xB0, 0x38, 0xC1, 0xDC, 0x1C, 0x60, 0xC6, 0x0C, 137 | 0x38, 0x63, 0x0E, 0x1C, 0x31, 0xC7, 0x06, 0x30, 0xE3, 0x03, 0x98, 0x31, 138 | 0x81, 0xCC, 0x19, 0xC0, 0x6E, 0x0E, 0xC0, 0x36, 0x03, 0x60, 0x1F, 0x01, 139 | 0xF0, 0x0F, 0x80, 0xF8, 0x03, 0x80, 0x38, 0x01, 0xC0, 0x1C, 0x00, 0x70, 140 | 0x03, 0x8E, 0x01, 0xC3, 0x80, 0x70, 0x70, 0x38, 0x0E, 0x1C, 0x01, 0x86, 141 | 0x00, 0x73, 0x80, 0x0F, 0xC0, 0x01, 0xE0, 0x00, 0x78, 0x00, 0x1E, 0x00, 142 | 0x0F, 0xC0, 0x07, 0x38, 0x03, 0x87, 0x00, 0xC1, 0xC0, 0x70, 0x38, 0x38, 143 | 0x07, 0x1C, 0x01, 0xCF, 0x00, 0x3C, 0xE0, 0x03, 0xB8, 0x03, 0x9E, 0x01, 144 | 0xC7, 0x01, 0xC1, 0xC0, 0xC0, 0xE0, 0xE0, 0x38, 0xE0, 0x0C, 0x70, 0x07, 145 | 0x70, 0x01, 0xF0, 0x00, 0xF8, 0x00, 0x38, 0x00, 0x1C, 0x00, 0x0E, 0x00, 146 | 0x07, 0x00, 0x03, 0x80, 0x01, 0xC0, 0x00, 0xE0, 0x00, 0x70, 0x00, 0xFF, 147 | 0xFD, 0xFF, 0xFB, 0xFF, 0xF0, 0x01, 0xC0, 0x03, 0x80, 0x0E, 0x00, 0x38, 148 | 0x00, 0xE0, 0x03, 0xC0, 0x07, 0x00, 0x1C, 0x00, 0x70, 0x01, 0xC0, 0x03, 149 | 0x80, 0x0E, 0x00, 0x38, 0x00, 0xE0, 0x01, 0xFF, 0xFF, 0xFF, 0xF8, 0xFF, 150 | 0xFC, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 151 | 0x0C, 0x30, 0xC3, 0xFF, 0xC0, 0xE0, 0x18, 0x07, 0x00, 0xC0, 0x30, 0x0E, 152 | 0x01, 0x80, 0x60, 0x1C, 0x03, 0x00, 0xC0, 0x38, 0x06, 0x01, 0x80, 0x30, 153 | 0x0C, 0x03, 0x80, 0x60, 0x1C, 0xFF, 0xF1, 0xC7, 0x1C, 0x71, 0xC7, 0x1C, 154 | 0x71, 0xC7, 0x1C, 0x71, 0xC7, 0x1C, 0x71, 0xC7, 0x1F, 0xFF, 0xC0, 0x0E, 155 | 0x00, 0xF0, 0x0F, 0x01, 0xB8, 0x19, 0x83, 0x98, 0x30, 0xC7, 0x0C, 0x60, 156 | 0x66, 0x06, 0xC0, 0x7C, 0x03, 0xFF, 0xFF, 0xFF, 0xF0, 0xE1, 0xC3, 0x07, 157 | 0x1F, 0x81, 0xFF, 0x1C, 0x3D, 0xC0, 0xE0, 0x03, 0x00, 0x18, 0xFF, 0xCF, 158 | 0xFE, 0xE0, 0x36, 0x01, 0xB0, 0x1D, 0xC1, 0xF7, 0xF9, 0x9F, 0x8C, 0x70, 159 | 0x00, 0xE0, 0x01, 0xC0, 0x03, 0x80, 0x07, 0x00, 0x0E, 0x7C, 0x1D, 0xFE, 160 | 0x3E, 0x1E, 0x78, 0x1C, 0xE0, 0x1D, 0xC0, 0x3B, 0x80, 0x77, 0x00, 0xEE, 161 | 0x01, 0xDC, 0x03, 0xBC, 0x06, 0xFC, 0x3D, 0xDF, 0xF3, 0x9F, 0xC0, 0x0F, 162 | 0xC1, 0xFF, 0x1E, 0x1C, 0xE0, 0x7E, 0x01, 0xF0, 0x03, 0x80, 0x1C, 0x00, 163 | 0xE0, 0x07, 0x00, 0xDC, 0x0E, 0xF0, 0xF3, 0xFF, 0x0F, 0xF0, 0x00, 0x1C, 164 | 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, 0x01, 0xC1, 0xF3, 0x8F, 0xF7, 0x3C, 165 | 0x3E, 0x70, 0x3D, 0xC0, 0x3B, 0x80, 0x77, 0x00, 0xEE, 0x01, 0xDC, 0x03, 166 | 0xB8, 0x07, 0x38, 0x1E, 0x78, 0x7E, 0x7F, 0xDC, 0x7F, 0x38, 0x0F, 0xC1, 167 | 0xFF, 0x1E, 0x1C, 0xE0, 0x7E, 0x01, 0xF0, 0x0F, 0xFF, 0xFF, 0xFF, 0xE0, 168 | 0x07, 0x00, 0x18, 0x0E, 0xF0, 0xF3, 0xFF, 0x07, 0xF0, 0x0F, 0x8F, 0xC7, 169 | 0x03, 0x81, 0xC7, 0xFF, 0xFE, 0x38, 0x1C, 0x0E, 0x07, 0x03, 0x81, 0xC0, 170 | 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x00, 0x0F, 0x8E, 0x7F, 0xDD, 0xF1, 171 | 0xFB, 0x81, 0xEE, 0x01, 0xDC, 0x03, 0xB8, 0x07, 0x70, 0x0E, 0xE0, 0x1D, 172 | 0xC0, 0x39, 0xC0, 0xF3, 0xC3, 0xE3, 0xFD, 0xC1, 0xF3, 0x80, 0x07, 0x70, 173 | 0x0C, 0xE0, 0x38, 0xF1, 0xF0, 0xFF, 0x80, 0x7C, 0x00, 0xC0, 0x06, 0x00, 174 | 0x30, 0x01, 0x80, 0x0C, 0x00, 0x63, 0xE3, 0x7F, 0x9F, 0x1E, 0xE0, 0x77, 175 | 0x01, 0xF0, 0x0F, 0x80, 0x7C, 0x03, 0xE0, 0x1F, 0x00, 0xF8, 0x07, 0xC0, 176 | 0x3E, 0x01, 0xF0, 0x0E, 0x6E, 0xE0, 0x07, 0x77, 0x77, 0x77, 0x77, 0x77, 177 | 0x77, 0x70, 0x18, 0xE3, 0x80, 0x00, 0x71, 0xC7, 0x1C, 0x71, 0xC7, 0x1C, 178 | 0x71, 0xC7, 0x1C, 0x71, 0xC7, 0x1C, 0x71, 0xFE, 0xC0, 0x06, 0x00, 0x30, 179 | 0x01, 0x80, 0x0C, 0x00, 0x60, 0x3B, 0x03, 0x98, 0x38, 0xC3, 0x86, 0x38, 180 | 0x33, 0x01, 0xF8, 0x0F, 0xE0, 0x73, 0x83, 0x0E, 0x18, 0x38, 0xC0, 0xE6, 181 | 0x07, 0xB0, 0x1E, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0xE3, 182 | 0xE1, 0xF1, 0xCF, 0xE7, 0xFB, 0xF1, 0xFC, 0x73, 0xC1, 0xE0, 0x77, 0x01, 183 | 0xC0, 0xEE, 0x03, 0x80, 0xDC, 0x07, 0x01, 0xB8, 0x0E, 0x03, 0x70, 0x1C, 184 | 0x06, 0xE0, 0x38, 0x0D, 0xC0, 0x70, 0x1B, 0x80, 0xE0, 0x37, 0x01, 0xC0, 185 | 0x6E, 0x03, 0x80, 0xC0, 0xE3, 0xE3, 0x9F, 0xEF, 0xC7, 0xDE, 0x07, 0x70, 186 | 0x0D, 0xC0, 0x37, 0x00, 0xDC, 0x03, 0x70, 0x0D, 0xC0, 0x37, 0x00, 0xDC, 187 | 0x03, 0x70, 0x0D, 0xC0, 0x30, 0x0F, 0xC0, 0xFF, 0xC7, 0x87, 0x9C, 0x0E, 188 | 0xE0, 0x1F, 0x80, 0x7E, 0x00, 0xF8, 0x03, 0xE0, 0x1F, 0x80, 0x77, 0x01, 189 | 0x9E, 0x1E, 0x3F, 0xF0, 0x3F, 0x80, 0xE3, 0xE1, 0xDF, 0xF3, 0xF0, 0xF3, 190 | 0xC0, 0xE7, 0x00, 0xEE, 0x01, 0xDC, 0x03, 0xB8, 0x07, 0x70, 0x0E, 0xE0, 191 | 0x1D, 0xE0, 0x33, 0xE1, 0xE7, 0x7F, 0x8E, 0x7E, 0x1C, 0x00, 0x38, 0x00, 192 | 0x70, 0x00, 0xE0, 0x01, 0xC0, 0x00, 0x0F, 0x8E, 0x7F, 0xDD, 0xE1, 0xFB, 193 | 0x81, 0xEE, 0x01, 0xDC, 0x03, 0xB8, 0x07, 0x70, 0x0E, 0xE0, 0x1D, 0xC0, 194 | 0x39, 0xC0, 0xF3, 0xC3, 0xE3, 0xFD, 0xC3, 0xF3, 0x80, 0x07, 0x00, 0x0E, 195 | 0x00, 0x1C, 0x00, 0x38, 0x00, 0x70, 0xE1, 0xF9, 0xFE, 0xE0, 0xE0, 0x38, 196 | 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x00, 197 | 0x1F, 0x83, 0xFC, 0x70, 0xE6, 0x07, 0x60, 0x07, 0x00, 0x7F, 0x81, 0xFE, 198 | 0x00, 0xF0, 0x07, 0xE0, 0x7E, 0x07, 0x7F, 0xE3, 0xFC, 0x1C, 0x0E, 0x07, 199 | 0x03, 0x8F, 0xFF, 0xFC, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x03, 0x81, 0xC0, 200 | 0xE0, 0x70, 0x38, 0x1F, 0x87, 0xC0, 0xC0, 0x3B, 0x00, 0xEC, 0x03, 0xB0, 201 | 0x0E, 0xC0, 0x3B, 0x00, 0xEC, 0x03, 0xB0, 0x0E, 0xC0, 0x3B, 0x00, 0xEE, 202 | 0x03, 0xBC, 0x3F, 0x7F, 0xDC, 0xFE, 0x70, 0xE0, 0x0E, 0xC0, 0x19, 0xC0, 203 | 0x73, 0x80, 0xC3, 0x83, 0x87, 0x06, 0x06, 0x1C, 0x0E, 0x38, 0x0C, 0x60, 204 | 0x1D, 0xC0, 0x3B, 0x00, 0x3E, 0x00, 0x78, 0x00, 0x70, 0x00, 0xE0, 0x38, 205 | 0x0D, 0x80, 0xE0, 0x77, 0x07, 0xC1, 0xDC, 0x1B, 0x06, 0x30, 0x6C, 0x18, 206 | 0xE3, 0xB8, 0xE3, 0x8C, 0x63, 0x86, 0x31, 0x8C, 0x1D, 0xC7, 0x70, 0x76, 207 | 0x0D, 0xC0, 0xF8, 0x36, 0x03, 0xE0, 0xF8, 0x0F, 0x01, 0xE0, 0x1C, 0x07, 208 | 0x00, 0x70, 0x1C, 0xC0, 0xE3, 0x83, 0x07, 0x1C, 0x0E, 0xE0, 0x1F, 0x00, 209 | 0x78, 0x01, 0xE0, 0x07, 0xC0, 0x3B, 0x81, 0xC7, 0x0E, 0x0C, 0x30, 0x3B, 210 | 0xC0, 0x70, 0xE0, 0x0E, 0xC0, 0x19, 0xC0, 0x71, 0x80, 0xC3, 0x83, 0x87, 211 | 0x06, 0x07, 0x1C, 0x0E, 0x38, 0x0C, 0x60, 0x1D, 0xC0, 0x1B, 0x00, 0x3E, 212 | 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, 0x01, 0x80, 0x07, 0x00, 0x7C, 0x00, 213 | 0xF8, 0x00, 0xFF, 0xFF, 0xFC, 0x03, 0x80, 0xE0, 0x38, 0x06, 0x01, 0xC0, 214 | 0x70, 0x1C, 0x07, 0x01, 0xC0, 0x30, 0x0F, 0xFF, 0xFF, 0xC0, 0x01, 0x0F, 215 | 0x1F, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x78, 0xE0, 0xF0, 0x38, 216 | 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x1C, 0x1F, 0x07, 0xFF, 0xFF, 0xFF, 217 | 0xFF, 0xFF, 0xFC, 0x80, 0xF0, 0xF8, 0x38, 0x18, 0x18, 0x18, 0x18, 0x18, 218 | 0x18, 0x1C, 0x0F, 0x0F, 0x1C, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x38, 219 | 0xF0, 0xE0, 0x7C, 0x1F, 0xFF, 0x87, 0xE8, 0x0C }; 220 | 221 | const GFXglyph FunnelDisplay_Regular14pt7bGlyphs[] PROGMEM = { 222 | { 0, 1, 1, 7, 0, 0 }, // 0x20 ' ' 223 | { 1, 3, 19, 7, 2, -18 }, // 0x21 '!' 224 | { 9, 8, 7, 11, 2, -18 }, // 0x22 '"' 225 | { 16, 15, 19, 15, 0, -18 }, // 0x23 '#' 226 | { 52, 14, 22, 16, 1, -19 }, // 0x24 '$' 227 | { 91, 21, 19, 24, 1, -18 }, // 0x25 '%' 228 | { 141, 17, 19, 18, 1, -18 }, // 0x26 '&' 229 | { 182, 2, 7, 6, 2, -18 }, // 0x27 ''' 230 | { 184, 6, 23, 8, 1, -18 }, // 0x28 '(' 231 | { 202, 6, 23, 8, 1, -18 }, // 0x29 ')' 232 | { 220, 12, 11, 14, 1, -18 }, // 0x2A '*' 233 | { 237, 12, 13, 16, 2, -15 }, // 0x2B '+' 234 | { 257, 4, 7, 6, 1, -2 }, // 0x2C ',' 235 | { 261, 6, 2, 8, 1, -7 }, // 0x2D '-' 236 | { 263, 4, 3, 6, 1, -2 }, // 0x2E '.' 237 | { 265, 10, 19, 10, 0, -18 }, // 0x2F '/' 238 | { 289, 14, 19, 16, 1, -18 }, // 0x30 '0' 239 | { 323, 12, 19, 16, 2, -18 }, // 0x31 '1' 240 | { 352, 13, 19, 16, 1, -18 }, // 0x32 '2' 241 | { 383, 13, 19, 16, 1, -18 }, // 0x33 '3' 242 | { 414, 15, 19, 16, 0, -18 }, // 0x34 '4' 243 | { 450, 13, 19, 16, 1, -18 }, // 0x35 '5' 244 | { 481, 14, 19, 16, 1, -18 }, // 0x36 '6' 245 | { 515, 14, 19, 16, 1, -18 }, // 0x37 '7' 246 | { 549, 14, 19, 16, 1, -18 }, // 0x38 '8' 247 | { 583, 13, 19, 16, 1, -18 }, // 0x39 '9' 248 | { 614, 4, 11, 8, 2, -11 }, // 0x3A ':' 249 | { 620, 4, 15, 8, 2, -11 }, // 0x3B ';' 250 | { 628, 12, 12, 16, 2, -14 }, // 0x3C '<' 251 | { 646, 12, 7, 16, 2, -12 }, // 0x3D '=' 252 | { 657, 12, 12, 16, 2, -14 }, // 0x3E '>' 253 | { 675, 12, 19, 13, 1, -18 }, // 0x3F '?' 254 | { 704, 24, 22, 26, 1, -18 }, // 0x40 '@' 255 | { 770, 19, 19, 19, 0, -18 }, // 0x41 'A' 256 | { 816, 16, 19, 19, 2, -18 }, // 0x42 'B' 257 | { 854, 17, 19, 19, 1, -18 }, // 0x43 'C' 258 | { 895, 17, 19, 20, 2, -18 }, // 0x44 'D' 259 | { 936, 13, 19, 16, 2, -18 }, // 0x45 'E' 260 | { 967, 13, 19, 15, 2, -18 }, // 0x46 'F' 261 | { 998, 18, 19, 20, 1, -18 }, // 0x47 'G' 262 | { 1041, 16, 19, 20, 2, -18 }, // 0x48 'H' 263 | { 1079, 3, 19, 7, 2, -18 }, // 0x49 'I' 264 | { 1087, 8, 19, 10, 0, -18 }, // 0x4A 'J' 265 | { 1106, 16, 19, 18, 2, -18 }, // 0x4B 'K' 266 | { 1144, 13, 19, 15, 2, -18 }, // 0x4C 'L' 267 | { 1175, 21, 19, 25, 2, -18 }, // 0x4D 'M' 268 | { 1225, 17, 19, 21, 2, -18 }, // 0x4E 'N' 269 | { 1266, 19, 19, 21, 1, -18 }, // 0x4F 'O' 270 | { 1312, 15, 19, 18, 2, -18 }, // 0x50 'P' 271 | { 1348, 19, 23, 21, 1, -18 }, // 0x51 'Q' 272 | { 1403, 15, 19, 19, 2, -18 }, // 0x52 'R' 273 | { 1439, 15, 19, 17, 1, -18 }, // 0x53 'S' 274 | { 1475, 15, 19, 17, 1, -18 }, // 0x54 'T' 275 | { 1511, 16, 19, 20, 2, -18 }, // 0x55 'U' 276 | { 1549, 19, 19, 19, 0, -18 }, // 0x56 'V' 277 | { 1595, 25, 19, 27, 1, -18 }, // 0x57 'W' 278 | { 1655, 18, 19, 18, 0, -18 }, // 0x58 'X' 279 | { 1698, 17, 19, 17, 0, -18 }, // 0x59 'Y' 280 | { 1739, 15, 19, 16, 1, -18 }, // 0x5A 'Z' 281 | { 1775, 6, 23, 9, 2, -18 }, // 0x5B '[' 282 | { 1793, 10, 19, 10, 0, -18 }, // 0x5C '\' 283 | { 1817, 6, 23, 9, 1, -18 }, // 0x5D ']' 284 | { 1835, 12, 12, 16, 2, -14 }, // 0x5E '^' 285 | { 1853, 14, 2, 14, 0, 3 }, // 0x5F '_' 286 | { 1857, 6, 4, 14, 4, -19 }, // 0x60 '`' 287 | { 1860, 13, 14, 16, 1, -13 }, // 0x61 'a' 288 | { 1883, 15, 19, 17, 1, -18 }, // 0x62 'b' 289 | { 1919, 13, 14, 15, 1, -13 }, // 0x63 'c' 290 | { 1942, 15, 19, 17, 1, -18 }, // 0x64 'd' 291 | { 1978, 13, 14, 16, 1, -13 }, // 0x65 'e' 292 | { 2001, 9, 19, 10, 0, -18 }, // 0x66 'f' 293 | { 2023, 15, 20, 17, 1, -13 }, // 0x67 'g' 294 | { 2061, 13, 19, 17, 2, -18 }, // 0x68 'h' 295 | { 2092, 4, 19, 7, 1, -18 }, // 0x69 'i' 296 | { 2102, 6, 24, 6, -1, -18 }, // 0x6A 'j' 297 | { 2120, 13, 19, 15, 2, -18 }, // 0x6B 'k' 298 | { 2151, 3, 19, 7, 2, -18 }, // 0x6C 'l' 299 | { 2159, 23, 14, 26, 1, -13 }, // 0x6D 'm' 300 | { 2200, 14, 14, 17, 1, -13 }, // 0x6E 'n' 301 | { 2225, 14, 14, 16, 1, -13 }, // 0x6F 'o' 302 | { 2250, 15, 19, 17, 1, -13 }, // 0x70 'p' 303 | { 2286, 15, 19, 17, 1, -13 }, // 0x71 'q' 304 | { 2322, 10, 14, 11, 1, -13 }, // 0x72 'r' 305 | { 2340, 12, 14, 14, 1, -13 }, // 0x73 's' 306 | { 2361, 9, 18, 10, 0, -17 }, // 0x74 't' 307 | { 2382, 14, 14, 17, 2, -13 }, // 0x75 'u' 308 | { 2407, 15, 14, 15, 0, -13 }, // 0x76 'v' 309 | { 2434, 22, 14, 23, 0, -13 }, // 0x77 'w' 310 | { 2473, 14, 14, 15, 0, -13 }, // 0x78 'x' 311 | { 2498, 15, 19, 15, 0, -13 }, // 0x79 'y' 312 | { 2534, 11, 14, 13, 1, -13 }, // 0x7A 'z' 313 | { 2554, 8, 23, 10, 1, -18 }, // 0x7B '{' 314 | { 2577, 2, 23, 14, 6, -18 }, // 0x7C '|' 315 | { 2583, 8, 23, 10, 1, -18 }, // 0x7D '}' 316 | { 2606, 12, 4, 16, 2, -10 } }; // 0x7E '~' 317 | 318 | const GFXfont FunnelDisplay_Regular14pt7b PROGMEM = { 319 | (uint8_t *)FunnelDisplay_Regular14pt7bBitmaps, 320 | (GFXglyph *)FunnelDisplay_Regular14pt7bGlyphs, 321 | 0x20, 0x7E, 34 }; 322 | 323 | // Approx. 3284 bytes 324 | -------------------------------------------------------------------------------- /src/fonts/HelvetiPixel16pt7b.h: -------------------------------------------------------------------------------- 1 | const uint8_t HelvetiPixel16pt7bBitmaps[] PROGMEM = { 2 | 0x00, 0xFF, 0xFF, 0xFF, 0x0F, 0xCF, 0x3C, 0xF3, 0xCF, 0x30, 0x0C, 0xC0, 3 | 0xCC, 0x0C, 0xC0, 0xCC, 0xFF, 0xFF, 0xFF, 0x33, 0x03, 0x30, 0x33, 0x03, 4 | 0x30, 0xFF, 0xFF, 0xFF, 0x33, 0x03, 0x30, 0x33, 0x03, 0x30, 0x0C, 0x0F, 5 | 0xC3, 0xF3, 0x33, 0xCC, 0xF3, 0x0C, 0xC0, 0xFC, 0x3F, 0x03, 0x30, 0xCF, 6 | 0x33, 0xCC, 0xCF, 0xC3, 0xF0, 0x30, 0x0C, 0x00, 0x30, 0x30, 0x60, 0x63, 7 | 0x30, 0xC6, 0x61, 0x8C, 0xCC, 0x19, 0x98, 0x0C, 0x30, 0x18, 0x60, 0x03, 8 | 0x0C, 0x06, 0x18, 0x0C, 0xD8, 0x19, 0xB0, 0xC3, 0x61, 0x86, 0xC3, 0x03, 9 | 0x06, 0x06, 0x0F, 0x00, 0x3C, 0x03, 0x0C, 0x0C, 0x30, 0x30, 0xC0, 0xC3, 10 | 0x00, 0xF0, 0x03, 0xC0, 0x33, 0x0C, 0xCC, 0x3C, 0x0F, 0x30, 0x3C, 0xC0, 11 | 0xF3, 0x03, 0xC3, 0xF0, 0xCF, 0xC3, 0xFF, 0xF0, 0x0C, 0x33, 0x0C, 0x30, 12 | 0xCC, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x03, 0x0C, 0x30, 0xC0, 0xC3, 0xC3, 13 | 0x03, 0x0C, 0x30, 0xC0, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x33, 0x0C, 0x30, 14 | 0xCC, 0x30, 0xCC, 0xF3, 0x33, 0xF0, 0xFC, 0xCC, 0xF3, 0x30, 0x0C, 0x03, 15 | 0x00, 0xC0, 0x30, 0xFF, 0xFF, 0xF0, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0x33, 16 | 0x33, 0xCC, 0xFF, 0xF0, 0xF0, 0x0C, 0x30, 0xC3, 0x0C, 0x33, 0x0C, 0x30, 17 | 0xC3, 0x0C, 0xC3, 0x0C, 0x30, 0xC3, 0x00, 0x3F, 0x0F, 0xCC, 0x0F, 0x03, 18 | 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 19 | 0x33, 0xF0, 0xFC, 0x0C, 0x33, 0xCF, 0xCF, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 20 | 0x0C, 0x30, 0xC3, 0x3F, 0x0F, 0xCC, 0x0F, 0x03, 0x00, 0xC0, 0x30, 0x30, 21 | 0x0C, 0x0C, 0x03, 0x03, 0x00, 0xC0, 0xC0, 0x30, 0x0F, 0xFF, 0xFF, 0x3F, 22 | 0x0F, 0xCC, 0x0F, 0x03, 0x00, 0xC0, 0x30, 0xF0, 0x3C, 0x00, 0xC0, 0x30, 23 | 0x0C, 0x03, 0xC0, 0xF0, 0x33, 0xF0, 0xFC, 0x00, 0xC0, 0x0C, 0x03, 0xC0, 24 | 0x3C, 0x0C, 0xC0, 0xCC, 0x30, 0xC3, 0x0C, 0xC0, 0xCC, 0x0C, 0xFF, 0xFF, 25 | 0xFF, 0x00, 0xC0, 0x0C, 0x00, 0xC0, 0x0C, 0xFF, 0xFF, 0xFC, 0x03, 0x00, 26 | 0xC0, 0x30, 0x0F, 0xF3, 0xFC, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0xC0, 0xF0, 27 | 0x33, 0xF0, 0xFC, 0x3F, 0x0F, 0xCC, 0x0F, 0x03, 0xC0, 0x30, 0x0F, 0xF3, 28 | 0xFC, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x33, 0xF0, 0xFC, 0xFF, 29 | 0xFF, 0xF0, 0x0C, 0x03, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x0C, 0x03, 0x00, 30 | 0xC0, 0x30, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x3F, 0x0F, 0xCC, 0x0F, 0x03, 31 | 0xC0, 0xF0, 0x33, 0xF0, 0xFC, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 32 | 0x33, 0xF0, 0xFC, 0x3F, 0x0F, 0xCC, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 33 | 0x03, 0x3F, 0xCF, 0xF0, 0x0C, 0x03, 0x00, 0xC0, 0x33, 0xF0, 0xFC, 0xF0, 34 | 0x00, 0xF0, 0x33, 0x00, 0x00, 0x00, 0x33, 0x33, 0xCC, 0x00, 0xF0, 0x0F, 35 | 0x0F, 0x00, 0xF0, 0xF0, 0x0F, 0x00, 0x0F, 0x00, 0xF0, 0x00, 0xF0, 0x0F, 36 | 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0xFF, 0xFF, 0xF0, 0xF0, 0x0F, 0x00, 0x0F, 37 | 0x00, 0xF0, 0x00, 0xF0, 0x0F, 0x0F, 0x00, 0xF0, 0xF0, 0x0F, 0x00, 0x3F, 38 | 0x0F, 0xCC, 0x0F, 0x03, 0xC0, 0xF0, 0x30, 0x30, 0x0C, 0x0C, 0x03, 0x00, 39 | 0xC0, 0x30, 0x00, 0x00, 0x00, 0xC0, 0x30, 0x0F, 0xF0, 0x1F, 0xE0, 0xC0, 40 | 0x31, 0x80, 0x6C, 0x3F, 0x78, 0x7E, 0xF3, 0x0D, 0xE6, 0x1B, 0xCC, 0x37, 41 | 0x98, 0x6F, 0x0F, 0xF6, 0x1F, 0xE3, 0x00, 0x66, 0x00, 0xC3, 0xFF, 0x07, 42 | 0xFE, 0x03, 0x00, 0x0C, 0x00, 0x30, 0x00, 0xC0, 0x0C, 0xC0, 0x33, 0x00, 43 | 0xCC, 0x03, 0x30, 0x30, 0x30, 0xC0, 0xC3, 0xFF, 0x0F, 0xFC, 0xC0, 0x0F, 44 | 0x00, 0x3C, 0x00, 0xF0, 0x03, 0xFF, 0x3F, 0xCC, 0x0F, 0x03, 0xC0, 0xF0, 45 | 0x3F, 0xF3, 0xFC, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3F, 0xF3, 46 | 0xFC, 0x0F, 0xF0, 0x3F, 0xC3, 0x00, 0xCC, 0x03, 0xC0, 0x03, 0x00, 0x0C, 47 | 0x00, 0x30, 0x00, 0xC0, 0x03, 0x00, 0x0C, 0x00, 0x30, 0x00, 0x30, 0x0C, 48 | 0xC0, 0x30, 0xFF, 0x03, 0xFC, 0xFF, 0x0F, 0xF0, 0xC0, 0xCC, 0x0C, 0xC0, 49 | 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 50 | 0xCC, 0x0C, 0xFF, 0x0F, 0xF0, 0xFF, 0xFF, 0xFC, 0x03, 0x00, 0xC0, 0x30, 51 | 0x0F, 0xF3, 0xFC, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0F, 0xFF, 52 | 0xFF, 0xFF, 0xFF, 0xFC, 0x03, 0x00, 0xC0, 0x30, 0x0F, 0xF3, 0xFC, 0xC0, 53 | 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0x0F, 0xF0, 0x3F, 54 | 0xC3, 0x00, 0xCC, 0x03, 0xC0, 0x03, 0x00, 0x0C, 0x00, 0x30, 0x00, 0xC0, 55 | 0xFF, 0x03, 0xFC, 0x00, 0xF0, 0x03, 0x30, 0x0C, 0xC0, 0x30, 0xFF, 0x03, 56 | 0xFC, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xFF, 0xFF, 57 | 0xFF, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 58 | 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 59 | 0x03, 0x03, 0x03, 0x03, 0x03, 0xC3, 0xC3, 0x3C, 0x3C, 0xC0, 0xCC, 0x0C, 60 | 0xC3, 0x0C, 0x30, 0xCC, 0x0C, 0xC0, 0xF0, 0x0F, 0x00, 0xCC, 0x0C, 0xC0, 61 | 0xC3, 0x0C, 0x30, 0xC0, 0xCC, 0x0C, 0xC0, 0x3C, 0x03, 0xC0, 0x30, 0x0C, 62 | 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 63 | 0xC0, 0x30, 0x0F, 0xFF, 0xFF, 0xC0, 0x0F, 0x00, 0x3F, 0x03, 0xFC, 0x0F, 64 | 0xF0, 0x3F, 0xC0, 0xFC, 0xCC, 0xF3, 0x33, 0xCC, 0xCF, 0x33, 0x3C, 0x30, 65 | 0xF0, 0xC3, 0xC3, 0x0F, 0x0C, 0x3C, 0x00, 0xF0, 0x03, 0xC0, 0xF0, 0x3F, 66 | 0x0F, 0xC3, 0xF0, 0xFC, 0x3C, 0xCF, 0x33, 0xCC, 0xF3, 0x3C, 0x3F, 0x0F, 67 | 0xC3, 0xF0, 0xFC, 0x0F, 0x03, 0x0F, 0xC0, 0x3F, 0x03, 0x03, 0x0C, 0x0C, 68 | 0xC0, 0x0F, 0x00, 0x3C, 0x00, 0xF0, 0x03, 0xC0, 0x0F, 0x00, 0x3C, 0x00, 69 | 0xF0, 0x03, 0x30, 0x30, 0xC0, 0xC0, 0xFC, 0x03, 0xF0, 0xFF, 0x3F, 0xCC, 70 | 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xFF, 0x3F, 0xCC, 0x03, 0x00, 71 | 0xC0, 0x30, 0x0C, 0x03, 0x00, 0x0F, 0xC0, 0x3F, 0x03, 0x03, 0x0C, 0x0C, 72 | 0xC0, 0x0F, 0x00, 0x3C, 0x00, 0xF0, 0x03, 0xC0, 0x0F, 0x00, 0x3C, 0x00, 73 | 0xF0, 0x03, 0x30, 0xF0, 0xC3, 0xC0, 0xFF, 0xC3, 0xFF, 0xFF, 0xCF, 0xFC, 74 | 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xFF, 0xCF, 0xFC, 75 | 0xC3, 0x0C, 0x30, 0xC0, 0xCC, 0x0C, 0xC0, 0x3C, 0x03, 0x3F, 0x0F, 0xCC, 76 | 0x0F, 0x03, 0xC0, 0x30, 0x03, 0xC0, 0xF0, 0x03, 0x00, 0xC0, 0x0C, 0x03, 77 | 0xC0, 0xF0, 0x33, 0xF0, 0xFC, 0xFF, 0xFF, 0xF0, 0xC0, 0x30, 0x0C, 0x03, 78 | 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 79 | 0x30, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 80 | 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0x3F, 0xC3, 81 | 0xFC, 0xC0, 0x0F, 0x00, 0x3C, 0x00, 0xF0, 0x03, 0x30, 0x30, 0xC0, 0xC3, 82 | 0x03, 0x0C, 0x0C, 0x0C, 0xC0, 0x33, 0x00, 0xCC, 0x03, 0x30, 0x03, 0x00, 83 | 0x0C, 0x00, 0x30, 0x00, 0xC0, 0xC0, 0xC1, 0xE0, 0x60, 0xF0, 0x30, 0x78, 84 | 0x18, 0x3C, 0x0C, 0x1E, 0x06, 0x0C, 0xCC, 0xCC, 0x66, 0x66, 0x33, 0x33, 85 | 0x19, 0x99, 0x8C, 0xCC, 0xC6, 0x66, 0x60, 0xC0, 0xC0, 0x60, 0x60, 0x30, 86 | 0x30, 0x18, 0x18, 0xC0, 0xF0, 0x33, 0x30, 0xCC, 0x33, 0x0C, 0xC0, 0xC0, 87 | 0x30, 0x0C, 0x03, 0x03, 0x30, 0xCC, 0x33, 0x0C, 0xCC, 0x0F, 0x03, 0xC0, 88 | 0xF0, 0x3C, 0x0F, 0x03, 0x33, 0x0C, 0xC3, 0x30, 0xCC, 0x0C, 0x03, 0x00, 89 | 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0xFF, 0xFF, 0xFF, 0x00, 0x30, 90 | 0x03, 0x00, 0xC0, 0x0C, 0x03, 0x00, 0x30, 0x0C, 0x00, 0xC0, 0x30, 0x03, 91 | 0x00, 0xC0, 0x0C, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xCC, 0xCC, 0xCC, 0xCC, 92 | 0xCC, 0xCC, 0xCC, 0xCC, 0xFF, 0xC3, 0x0C, 0x30, 0xC3, 0x03, 0x0C, 0x30, 93 | 0xC3, 0x0C, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xFF, 0x33, 0x33, 0x33, 0x33, 94 | 0x33, 0x33, 0x33, 0x33, 0xFF, 0x0C, 0x03, 0x03, 0x30, 0xCC, 0x33, 0x0C, 95 | 0xCC, 0x0F, 0x03, 0xC0, 0xF0, 0x30, 0xFF, 0xFF, 0xFF, 0xCC, 0x33, 0x3F, 96 | 0x0F, 0xCC, 0x0F, 0x03, 0x3F, 0xCF, 0xFC, 0x0F, 0x03, 0x3F, 0xCF, 0xF0, 97 | 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0F, 0xF3, 0xFC, 0xC0, 0xF0, 98 | 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3F, 0xF3, 0xFC, 0x3F, 0x0F, 0xCC, 0x0F, 99 | 0x03, 0xC0, 0x30, 0x0C, 0x0F, 0x03, 0x3F, 0x0F, 0xC0, 0x00, 0xC0, 0x30, 100 | 0x0C, 0x03, 0x00, 0xC0, 0x33, 0xFC, 0xFF, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 101 | 0xC0, 0xF0, 0x33, 0xFC, 0xFF, 0x3F, 0x0F, 0xCC, 0x0F, 0x03, 0xFF, 0xFF, 102 | 0xFC, 0x03, 0x00, 0x3F, 0xCF, 0xF0, 0x0C, 0x33, 0x0C, 0x30, 0xCF, 0xFF, 103 | 0x30, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x3F, 0xCF, 0xFC, 0x0F, 0x03, 0xC0, 104 | 0xF0, 0x3C, 0x0F, 0x03, 0x3F, 0xCF, 0xF0, 0x0C, 0x03, 0x3F, 0x0F, 0xC0, 105 | 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0F, 0xF3, 0xFC, 0xC0, 0xF0, 106 | 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xF0, 0xFF, 0xFF, 0xF0, 107 | 0x33, 0x00, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0xCC, 0xC0, 0xC0, 0xC0, 108 | 0xC0, 0xC0, 0xC0, 0xC3, 0xC3, 0xCC, 0xCC, 0xF0, 0xF0, 0xCC, 0xCC, 0xC3, 109 | 0xC3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0xF3, 0xF3, 0xCC, 0x30, 0xF0, 0xC3, 110 | 0xC3, 0x0F, 0x0C, 0x3C, 0x30, 0xF0, 0xC3, 0xC3, 0x0F, 0x0C, 0x30, 0xFF, 111 | 0x3F, 0xCC, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x30, 112 | 0x3F, 0x0F, 0xCC, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0x3F, 0x0F, 113 | 0xC0, 0xFF, 0x3F, 0xCC, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xFF, 114 | 0x3F, 0xCC, 0x03, 0x00, 0xC0, 0x30, 0x00, 0x3F, 0xCF, 0xFC, 0x0F, 0x03, 115 | 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0x3F, 0xCF, 0xF0, 0x0C, 0x03, 0x00, 0xC0, 116 | 0x30, 0xCF, 0xCF, 0xF0, 0xF0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0x3F, 117 | 0x3F, 0xC0, 0xC0, 0x3C, 0x3C, 0x03, 0x03, 0xFC, 0xFC, 0x30, 0xC3, 0x0C, 118 | 0xFF, 0xF3, 0x0C, 0x30, 0xC3, 0x0C, 0x0C, 0x30, 0xC0, 0xF0, 0x3C, 0x0F, 119 | 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0x3F, 0xCF, 0xF0, 0xC0, 0xF0, 0x3C, 120 | 0x0F, 0x03, 0x33, 0x0C, 0xC3, 0x30, 0xCC, 0x0C, 0x03, 0x00, 0xC3, 0x0F, 121 | 0x0C, 0x3C, 0x30, 0xF0, 0xC3, 0xCC, 0xCF, 0x33, 0x33, 0x03, 0x0C, 0x0C, 122 | 0x30, 0x30, 0xC0, 0xC0, 0xC0, 0xF0, 0x33, 0x30, 0xCC, 0x0C, 0x03, 0x03, 123 | 0x30, 0xCC, 0xC0, 0xF0, 0x30, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0x33, 0x0C, 124 | 0xC3, 0x30, 0xCC, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0xF0, 0x3C, 0x00, 0xFF, 125 | 0xFF, 0xF0, 0x30, 0x0C, 0x0C, 0x03, 0x03, 0x00, 0xC0, 0xFF, 0xFF, 0xF0, 126 | 0x0C, 0x33, 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xCC, 0x30, 0x30, 0xC3, 0x0C, 127 | 0x30, 0xC3, 0x0C, 0x0C, 0x30, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC3, 0x03, 128 | 0x0C, 0x30, 0xC3, 0x0C, 0x30, 0xC0, 0xC3, 0x30, 0xC3, 0x0C, 0x30, 0xC3, 129 | 0x0C, 0xC3, 0x00, 0x33, 0x33, 0xCC, 0xCC }; 130 | 131 | const GFXglyph HelvetiPixel16pt7bGlyphs[] PROGMEM = { 132 | { 0, 1, 1, 10, 0, 0 }, // 0x20 ' ' 133 | { 1, 2, 16, 6, 2, -15 }, // 0x21 '!' 134 | { 5, 6, 6, 8, 0, -15 }, // 0x22 '"' 135 | { 10, 12, 16, 14, 0, -15 }, // 0x23 '#' 136 | { 34, 10, 17, 12, 0, -16 }, // 0x24 '$' 137 | { 56, 15, 16, 19, 2, -15 }, // 0x25 '%' 138 | { 86, 14, 16, 16, 0, -15 }, // 0x26 '&' 139 | { 114, 2, 6, 4, 0, -15 }, // 0x27 ''' 140 | { 116, 6, 20, 8, 0, -15 }, // 0x28 '(' 141 | { 131, 6, 20, 8, 0, -15 }, // 0x29 ')' 142 | { 146, 10, 6, 12, 0, -13 }, // 0x2A '*' 143 | { 154, 10, 10, 12, 0, -11 }, // 0x2B '+' 144 | { 167, 4, 6, 6, 0, -1 }, // 0x2C ',' 145 | { 170, 6, 2, 8, 0, -5 }, // 0x2D '-' 146 | { 172, 2, 2, 6, 2, -1 }, // 0x2E '.' 147 | { 173, 6, 18, 8, 0, -15 }, // 0x2F '/' 148 | { 187, 10, 16, 12, 0, -15 }, // 0x30 '0' 149 | { 207, 6, 16, 10, 2, -15 }, // 0x31 '1' 150 | { 219, 10, 16, 12, 0, -15 }, // 0x32 '2' 151 | { 239, 10, 16, 12, 0, -15 }, // 0x33 '3' 152 | { 259, 12, 16, 14, 0, -15 }, // 0x34 '4' 153 | { 283, 10, 16, 12, 0, -15 }, // 0x35 '5' 154 | { 303, 10, 16, 12, 0, -15 }, // 0x36 '6' 155 | { 323, 10, 16, 12, 0, -15 }, // 0x37 '7' 156 | { 343, 10, 16, 12, 0, -15 }, // 0x38 '8' 157 | { 363, 10, 16, 12, 0, -15 }, // 0x39 '9' 158 | { 383, 2, 10, 6, 2, -9 }, // 0x3A ':' 159 | { 386, 4, 14, 6, 0, -9 }, // 0x3B ';' 160 | { 393, 12, 10, 14, 0, -11 }, // 0x3C '<' 161 | { 408, 10, 6, 12, 0, -9 }, // 0x3D '=' 162 | { 416, 12, 10, 14, 0, -11 }, // 0x3E '>' 163 | { 431, 10, 16, 12, 0, -15 }, // 0x3F '?' 164 | { 451, 15, 16, 19, 2, -13 }, // 0x40 '@' 165 | { 481, 14, 16, 16, 0, -15 }, // 0x41 'A' 166 | { 509, 10, 16, 14, 2, -15 }, // 0x42 'B' 167 | { 529, 14, 16, 16, 0, -15 }, // 0x43 'C' 168 | { 557, 12, 16, 16, 2, -15 }, // 0x44 'D' 169 | { 581, 10, 16, 14, 2, -15 }, // 0x45 'E' 170 | { 601, 10, 16, 14, 2, -15 }, // 0x46 'F' 171 | { 621, 14, 16, 16, 0, -15 }, // 0x47 'G' 172 | { 649, 12, 16, 16, 2, -15 }, // 0x48 'H' 173 | { 673, 2, 16, 6, 2, -15 }, // 0x49 'I' 174 | { 677, 8, 16, 10, 0, -15 }, // 0x4A 'J' 175 | { 693, 12, 16, 16, 2, -15 }, // 0x4B 'K' 176 | { 717, 10, 16, 14, 2, -15 }, // 0x4C 'L' 177 | { 737, 14, 16, 17, 2, -15 }, // 0x4D 'M' 178 | { 765, 10, 16, 14, 2, -15 }, // 0x4E 'N' 179 | { 785, 14, 16, 16, 0, -15 }, // 0x4F 'O' 180 | { 813, 10, 16, 14, 2, -15 }, // 0x50 'P' 181 | { 833, 14, 16, 16, 0, -15 }, // 0x51 'Q' 182 | { 861, 12, 16, 16, 2, -15 }, // 0x52 'R' 183 | { 885, 10, 16, 14, 2, -15 }, // 0x53 'S' 184 | { 905, 10, 16, 12, 0, -15 }, // 0x54 'T' 185 | { 925, 12, 16, 16, 2, -15 }, // 0x55 'U' 186 | { 949, 14, 16, 16, 0, -15 }, // 0x56 'V' 187 | { 977, 17, 16, 19, 0, -15 }, // 0x57 'W' 188 | { 1011, 10, 16, 14, 2, -15 }, // 0x58 'X' 189 | { 1031, 10, 16, 12, 0, -15 }, // 0x59 'Y' 190 | { 1051, 12, 16, 14, 0, -15 }, // 0x5A 'Z' 191 | { 1075, 4, 20, 8, 2, -15 }, // 0x5B '[' 192 | { 1085, 6, 18, 8, 0, -15 }, // 0x5C '\' 193 | { 1099, 4, 20, 6, 0, -15 }, // 0x5D ']' 194 | { 1109, 10, 10, 12, 0, -15 }, // 0x5E '^' 195 | { 1122, 12, 2, 14, 0, 3 }, // 0x5F '_' 196 | { 1125, 4, 4, 6, 0, -15 }, // 0x60 '`' 197 | { 1127, 10, 10, 12, 0, -9 }, // 0x61 'a' 198 | { 1140, 10, 16, 14, 2, -15 }, // 0x62 'b' 199 | { 1160, 10, 10, 12, 0, -9 }, // 0x63 'c' 200 | { 1173, 10, 16, 12, 0, -15 }, // 0x64 'd' 201 | { 1193, 10, 10, 12, 0, -9 }, // 0x65 'e' 202 | { 1206, 6, 16, 8, 0, -15 }, // 0x66 'f' 203 | { 1218, 10, 14, 12, 0, -9 }, // 0x67 'g' 204 | { 1236, 10, 16, 14, 2, -15 }, // 0x68 'h' 205 | { 1256, 2, 14, 6, 2, -13 }, // 0x69 'i' 206 | { 1260, 4, 18, 6, 0, -13 }, // 0x6A 'j' 207 | { 1269, 8, 16, 12, 2, -15 }, // 0x6B 'k' 208 | { 1285, 2, 16, 6, 2, -15 }, // 0x6C 'l' 209 | { 1289, 14, 10, 17, 2, -9 }, // 0x6D 'm' 210 | { 1307, 10, 10, 14, 2, -9 }, // 0x6E 'n' 211 | { 1320, 10, 10, 12, 0, -9 }, // 0x6F 'o' 212 | { 1333, 10, 14, 14, 2, -9 }, // 0x70 'p' 213 | { 1351, 10, 14, 12, 0, -9 }, // 0x71 'q' 214 | { 1369, 8, 10, 12, 2, -9 }, // 0x72 'r' 215 | { 1379, 8, 10, 10, 0, -9 }, // 0x73 's' 216 | { 1389, 6, 14, 8, 0, -13 }, // 0x74 't' 217 | { 1400, 10, 10, 14, 2, -9 }, // 0x75 'u' 218 | { 1413, 10, 10, 12, 0, -9 }, // 0x76 'v' 219 | { 1426, 14, 10, 16, 0, -9 }, // 0x77 'w' 220 | { 1444, 10, 10, 12, 0, -9 }, // 0x78 'x' 221 | { 1457, 10, 14, 12, 0, -9 }, // 0x79 'y' 222 | { 1475, 10, 10, 12, 0, -9 }, // 0x7A 'z' 223 | { 1488, 6, 22, 8, 0, -15 }, // 0x7B '{' 224 | { 1505, 2, 20, 6, 2, -15 }, // 0x7C '|' 225 | { 1510, 6, 22, 8, 0, -15 }, // 0x7D '}' 226 | { 1527, 8, 4, 12, 2, -9 } }; // 0x7E '~' 227 | 228 | const GFXfont HelvetiPixel16pt7b PROGMEM = { 229 | (uint8_t *)HelvetiPixel16pt7bBitmaps, 230 | (GFXglyph *)HelvetiPixel16pt7bGlyphs, 231 | 0x20, 0x7E, 30 }; 232 | 233 | // Approx. 2203 bytes 234 | -------------------------------------------------------------------------------- /src/gfx_utils.cpp: -------------------------------------------------------------------------------- 1 | #include "gfx_utils.h" 2 | #include "debug.h" 3 | 4 | void drawDebugCrosshair(DISPLAY_CLASS &display, int16_t x, int16_t y, int16_t length, uint16_t color) 5 | { 6 | #ifdef DEBUG 7 | display.drawFastHLine(x - length, y, 2 * length, color); 8 | display.drawFastVLine(x, y - length, 2 * length, color); 9 | #endif 10 | } 11 | 12 | void drawPattern(DISPLAY_CLASS &display, Pattern pattern, int16_t x, int16_t y, int16_t w, int16_t h) 13 | { 14 | const uint8_t *patternData = patterns[(int)pattern]; 15 | for (int16_t i = 0; i < h; i++) 16 | { 17 | for (int16_t j = 0; j < w; j++) 18 | { 19 | if (patternData[i % 8] & (0x80 >> (j % 8))) 20 | { 21 | display.drawPixel(x + j, y + i, GxEPD_BLACK); 22 | } 23 | } 24 | } 25 | } 26 | 27 | void drawPatternInRoundedArea(DISPLAY_CLASS &display, int16_t startX, int16_t startY, int16_t areaWidth, int16_t areaHeight, int16_t radius, Pattern patternNo) 28 | { 29 | const int16_t patternWidth = 8; 30 | const int16_t patternHeight = 8; 31 | const uint8_t *pattern = patterns[(int)patternNo]; 32 | 33 | // Pre-calculate squared radius for circle tests. 34 | const int16_t rSq = radius * radius; 35 | 36 | // Define centers for the four corners. 37 | const int16_t centerTL_x = startX + radius; 38 | const int16_t centerTL_y = startY + radius; 39 | const int16_t centerTR_x = startX + areaWidth - radius - 1; 40 | const int16_t centerTR_y = startY + radius; 41 | const int16_t centerBL_x = startX + radius; 42 | const int16_t centerBL_y = startY + areaHeight - radius - 1; 43 | const int16_t centerBR_x = startX + areaWidth - radius - 1; 44 | const int16_t centerBR_y = startY + areaHeight - radius - 1; 45 | 46 | // Loop through every pixel in the defined area. 47 | for (int16_t y = startY; y < startY + areaHeight; y++) 48 | { 49 | for (int16_t x = startX; x < startX + areaWidth; x++) 50 | { 51 | bool drawPixelFlag = true; 52 | // Top-left corner 53 | if (x < startX + radius && y < startY + radius) 54 | { 55 | int16_t dx = centerTL_x - x; 56 | int16_t dy = centerTL_y - y; 57 | if ((dx * dx + dy * dy) > rSq) 58 | drawPixelFlag = false; 59 | } 60 | // Top-right corner 61 | else if (x >= startX + areaWidth - radius && y < startY + radius) 62 | { 63 | int16_t dx = x - centerTR_x; 64 | int16_t dy = centerTR_y - y; 65 | if ((dx * dx + dy * dy) > rSq) 66 | drawPixelFlag = false; 67 | } 68 | // Bottom-left corner 69 | else if (x < startX + radius && y >= startY + areaHeight - radius) 70 | { 71 | int16_t dx = centerBL_x - x; 72 | int16_t dy = y - centerBL_y; 73 | if ((dx * dx + dy * dy) > rSq) 74 | drawPixelFlag = false; 75 | } 76 | // Bottom-right corner 77 | else if (x >= startX + areaWidth - radius && y >= startY + areaHeight - radius) 78 | { 79 | int16_t dx = x - centerBR_x; 80 | int16_t dy = y - centerBR_y; 81 | if ((dx * dx + dy * dy) > rSq) 82 | drawPixelFlag = false; 83 | } 84 | 85 | if (drawPixelFlag) 86 | { 87 | // Determine pattern coordinates. 88 | int patternX = (x - startX) % patternWidth; 89 | int patternY = (y - startY) % patternHeight; 90 | // Check the bit (assumes MSB first in each byte). 91 | if (pattern[patternY] & (0x80 >> patternX)) 92 | { 93 | display.drawPixel(x, y, GxEPD_BLACK); 94 | } 95 | } 96 | } 97 | } 98 | } 99 | 100 | void drawProgressBar(DISPLAY_CLASS &display, ProgressBarStyle style, int16_t x, int16_t y, int16_t width, int16_t height, int16_t radius, int16_t progress) 101 | { 102 | int16_t progressWidth; 103 | int16_t innerRadius = radius > 2 ? radius - 2 : 0; 104 | switch (style) 105 | { 106 | case ProgressBarStyle::Bordered: 107 | display.fillRoundRect(x, y, width, height, radius, GxEPD_WHITE); 108 | display.drawRoundRect(x, y, width, height, radius, GxEPD_BLACK); 109 | progressWidth = (width - 8) * progress / 100; 110 | 111 | // recalculate inner radius because it is smaller than the outer radius 112 | 113 | drawPatternInRoundedArea(display, x + 4, y + 4, progressWidth, height - 8, innerRadius, Pattern::Dots); 114 | break; 115 | case ProgressBarStyle::Borderless: 116 | display.fillRoundRect(x, y, width, height, radius, GxEPD_WHITE); 117 | progressWidth = width * progress / 100; 118 | 119 | display.fillRoundRect(x, y, progressWidth, height, radius, GxEPD_BLACK); 120 | break; 121 | } 122 | } 123 | 124 | Bounds getBounds(DISPLAY_CLASS &display, const char *text, const GFXfont *font) 125 | { 126 | display.setTextSize(1); 127 | display.setFont(font); 128 | 129 | int16_t x1, y1; 130 | uint16_t w, h; 131 | display.getTextBounds(text, 0, 0, &x1, &y1, &w, &h); 132 | 133 | return {x1, y1, w, h}; 134 | } 135 | 136 | Bounds drawText(DISPLAY_CLASS &display, const char *text, int16_t x, int16_t y, const GFXfont *font, uint16_t color) 137 | { 138 | display.setTextSize(1); 139 | display.setFont(font); 140 | display.setTextColor(color); 141 | 142 | int16_t x1, y1; 143 | uint16_t w, h; 144 | display.getTextBounds(text, 0, 0, &x1, &y1, &w, &h); 145 | display.setCursor(x, y); 146 | display.print(text); 147 | 148 | return {x, y, w, h}; 149 | } 150 | 151 | Bounds drawBottomAlignedText(DISPLAY_CLASS &display, const char *text, int16_t x, int16_t y, const GFXfont *font, uint16_t color) 152 | { 153 | display.setTextSize(1); 154 | display.setFont(font); 155 | display.setTextColor(color); 156 | 157 | int16_t x1, y1; 158 | uint16_t w, h; 159 | display.getTextBounds(text, 0, 0, &x1, &y1, &w, &h); 160 | display.setCursor(x, y + h); 161 | display.print(text); 162 | 163 | return {static_cast(x), static_cast(y - h), w, h}; 164 | } 165 | 166 | Bounds drawCenteredText(DISPLAY_CLASS &display, const char *text, int16_t x, int16_t y, const GFXfont *font, uint16_t color) 167 | { 168 | display.setTextSize(1); 169 | display.setFont(font); 170 | display.setTextColor(color); 171 | 172 | int16_t x1, y1; 173 | uint16_t w, h; 174 | display.getTextBounds(text, 0, 0, &x1, &y1, &w, &h); 175 | // Correct the y coordinate considering y1 offset (usually negative) 176 | int16_t correctedY = y - h / 2 - y1; 177 | 178 | display.setCursor(x - w / 2, correctedY); 179 | display.print(text); 180 | 181 | return {static_cast(x - w / 2), correctedY, w, h}; 182 | } 183 | -------------------------------------------------------------------------------- /src/gfx_utils.h: -------------------------------------------------------------------------------- 1 | #ifndef GFXUTILS_H 2 | #define GFXUTILS_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "defs.h" 10 | 11 | enum class ProgressBarStyle 12 | { 13 | Bordered, 14 | Borderless 15 | }; 16 | 17 | enum class Pattern 18 | { 19 | Solid, 20 | Stripes, 21 | Dots, 22 | Checkerboard, 23 | DiagonalStripes, 24 | CrossHatch, 25 | SparseDots, 26 | VerySparseDots 27 | }; 28 | 29 | // define 8x8 patterns 30 | const uint8_t pattern_solid[8] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; 31 | // refined stripes: wider bands (upper half and lower half) 32 | const uint8_t pattern_stripes[8] = {0xF0, 0xF0, 0xF0, 0xF0, 0x0F, 0x0F, 0x0F, 0x0F}; 33 | // refined dots: softer dot effect 34 | const uint8_t pattern_dots[8] = {0x88, 0x44, 0x22, 0x11, 0x11, 0x22, 0x44, 0x88}; 35 | // new sparse dots pattern: dots further apart 36 | const uint8_t pattern_sparse_dots[8] = {0x88, 0x00, 0x22, 0x00, 0x88, 0x00, 0x22, 0x00}; 37 | // new very sparse dots pattern: dots very far apart 38 | const uint8_t pattern_very_sparse_dots[8] = {0x88, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00}; 39 | // refined checkerboard: standard alternating bits 40 | const uint8_t pattern_checkerboard[8] = {0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55}; 41 | // new diagonal stripes: repeated diagonal bands 42 | const uint8_t pattern_diagonal_stripes[8] = {0xC0, 0x30, 0x0C, 0x03, 0xC0, 0x30, 0x0C, 0x03}; 43 | // new crosshatch: grid-like pattern with full horizontal bars on top, middle, and bottom 44 | const uint8_t pattern_crosshatch[8] = {0xFF, 0x92, 0x92, 0x92, 0xFF, 0x92, 0x92, 0xFF}; 45 | 46 | // put all patterns in an array 47 | const std::vector patterns = { 48 | pattern_solid, 49 | pattern_stripes, 50 | pattern_dots, 51 | pattern_checkerboard, 52 | pattern_diagonal_stripes, 53 | pattern_crosshatch, 54 | pattern_sparse_dots, 55 | pattern_very_sparse_dots}; 56 | 57 | void drawDebugCrosshair(DISPLAY_CLASS &display, int16_t x, int16_t y, int16_t length = 8, uint16_t color = GxEPD_BLACK); 58 | 59 | void drawPattern(DISPLAY_CLASS &display, Pattern pattern, int16_t x, int16_t y, int16_t w, int16_t h); 60 | 61 | void drawPatternInRoundedArea( 62 | DISPLAY_CLASS &display, 63 | int16_t startX, int16_t startY, 64 | int16_t areaWidth, int16_t areaHeight, 65 | int16_t radius, 66 | Pattern patternNo); 67 | 68 | void drawProgressBar( 69 | DISPLAY_CLASS &display, 70 | ProgressBarStyle style, 71 | int16_t x, 72 | int16_t y, 73 | int16_t width, 74 | int16_t height, 75 | int16_t radius, 76 | int16_t progress); 77 | 78 | struct Bounds 79 | { 80 | int16_t x; 81 | int16_t y; 82 | uint16_t w; 83 | uint16_t h; 84 | }; 85 | 86 | Bounds getBounds( 87 | DISPLAY_CLASS &display, 88 | const char *text, 89 | const GFXfont *font); 90 | 91 | Bounds drawText( 92 | DISPLAY_CLASS &display, 93 | const char *text, 94 | int16_t x, 95 | int16_t y, 96 | const GFXfont *font, 97 | uint16_t color); 98 | 99 | Bounds drawBottomAlignedText(DISPLAY_CLASS &display, const char *text, int16_t x, int16_t y, const GFXfont *font, uint16_t color); 100 | 101 | Bounds drawCenteredText(DISPLAY_CLASS &display, const char *text, int16_t x, int16_t y, const GFXfont *font, uint16_t color); 102 | 103 | #endif -------------------------------------------------------------------------------- /src/icon.h: -------------------------------------------------------------------------------- 1 | #ifndef ICON_H 2 | #define ICON_H 3 | 4 | #include 5 | 6 | class ScaledIcon 7 | { 8 | public: 9 | ScaledIcon(const unsigned char *data, uint16_t size) 10 | : data(data), size(size) {} 11 | 12 | const unsigned char *data; 13 | uint16_t size; 14 | }; 15 | 16 | class Icon 17 | { 18 | public: 19 | Icon(const unsigned char *icon192, const unsigned char *icon128, const unsigned char *icon64, const unsigned char *icon48) 20 | : icon192(icon192), icon128(icon128), icon64(icon64), icon48(icon48) {} 21 | 22 | const unsigned char *icon192; 23 | const unsigned char *icon128; 24 | const unsigned char *icon64; 25 | const unsigned char *icon48; 26 | 27 | ScaledIcon scaled(uint16_t size) 28 | { 29 | switch (size) 30 | { 31 | case 192: 32 | return ScaledIcon(icon192, 192); 33 | case 128: 34 | return ScaledIcon(icon128, 128); 35 | case 64: 36 | return ScaledIcon(icon64, 64); 37 | case 48: 38 | return ScaledIcon(icon48, 48); 39 | default: 40 | return ScaledIcon(icon48, 48); 41 | } 42 | } 43 | }; 44 | 45 | #endif // ICON_H -------------------------------------------------------------------------------- /src/icon_provider.cpp: -------------------------------------------------------------------------------- 1 | #include "icon_provider.h" 2 | 3 | // Initialize the static member 4 | IconProvider *IconProvider::instance = nullptr; -------------------------------------------------------------------------------- /src/icon_provider.h: -------------------------------------------------------------------------------- 1 | #ifndef ICON_PROVIDER_H 2 | #define ICON_PROVIDER_H 3 | 4 | #include "icon.h" 5 | #include "icons.h" 6 | #include "images.h" 7 | 8 | class IconProvider 9 | { 10 | private: 11 | static IconProvider *instance; 12 | bool lpeModeEnabled = false; 13 | 14 | public: 15 | static IconProvider *getInstance() 16 | { 17 | if (!instance) 18 | { 19 | instance = new IconProvider(); 20 | } 21 | return instance; 22 | } 23 | 24 | void setLpeMode(bool enabled) 25 | { 26 | lpeModeEnabled = enabled; 27 | } 28 | 29 | bool isLpeModeEnabled() const 30 | { 31 | return lpeModeEnabled; 32 | } 33 | 34 | const unsigned char *getTimerRunningBackgroundImage() 35 | { 36 | if (lpeModeEnabled) 37 | { 38 | return image_bg_lpe_bubble; 39 | } 40 | else 41 | { 42 | return image_bg_bubble; 43 | } 44 | } 45 | 46 | Icon *getPresetIcon(const char *name) 47 | { 48 | if (strcmp(name, "Coding") == 0) 49 | { 50 | if (lpeModeEnabled) 51 | { 52 | return &icon_lpehacker; 53 | } 54 | else 55 | { 56 | return &icon_coding; 57 | } 58 | } 59 | else if (strcmp(name, "Emails") == 0) 60 | { 61 | if (lpeModeEnabled) 62 | { 63 | return &icon_lpetantrum; 64 | } 65 | else 66 | { 67 | return &icon_email; 68 | } 69 | } 70 | else if (strcmp(name, "Focus") == 0) 71 | { 72 | if (lpeModeEnabled) 73 | { 74 | return &icon_lpethink; 75 | } 76 | else 77 | { 78 | return &icon_focus; 79 | } 80 | } 81 | 82 | return &icon_lpehacker; 83 | } 84 | }; 85 | 86 | #endif // ICON_PROVIDER_H -------------------------------------------------------------------------------- /src/icons.h: -------------------------------------------------------------------------------- 1 | #ifndef ICONS_H 2 | #define ICONS_H 3 | 4 | #include "icon.h" 5 | 6 | extern Icon icon_email; 7 | extern Icon icon_warning; 8 | extern Icon icon_focus; 9 | extern Icon icon_coffee; 10 | extern Icon icon_lpetantrum; 11 | extern Icon icon_lpehacker; 12 | extern Icon icon_checkmark; 13 | extern Icon icon_lpenote; 14 | extern Icon icon_lpethink; 15 | extern Icon icon_lpesip; 16 | extern Icon icon_coding; 17 | 18 | #endif // ICONS_H 19 | -------------------------------------------------------------------------------- /src/images.h: -------------------------------------------------------------------------------- 1 | #ifndef IMAGES_H 2 | #define IMAGES_H 3 | 4 | #include "images/image_bg_cat.h" 5 | #include "images/image_bg_stonks.h" 6 | #include "images/image_bg_splash.h" 7 | #include "images/image_bg_pablo.h" 8 | #include "images/image_bg_lpe_bubble.h" 9 | #include "images/image_bg_what_a_week.h" 10 | #include "images/image_bg_bubble.h" 11 | 12 | #endif // IMAGES_H 13 | -------------------------------------------------------------------------------- /src/led.cpp: -------------------------------------------------------------------------------- 1 | #include "led.h" 2 | #include "button.h" 3 | #include "timer.h" 4 | 5 | #define WS2812_PIN 25 6 | #define LED_TASK_STACK_SIZE 2048 7 | #define LED_TASK_PRIORITY 2 8 | #define LED_CORE 0 // Run on core 0 since core 1 is used by Arduino loop 9 | 10 | TaskHandle_t ledTaskHandle = NULL; 11 | static NeoPixelBus strip(1, WS2812_PIN); 12 | static NeoPixelAnimator animations(2); 13 | static volatile LedMode currentMode = LedMode::Off; 14 | static volatile LedMode lastMode; 15 | 16 | static unsigned long lastFlashTime = 0; 17 | static boolean flashState = false; 18 | 19 | static bool isEncoderTriggeredFlash = false; // Track which input triggered the flash 20 | 21 | struct MyAnimationState 22 | { 23 | RgbColor StartingColor; 24 | RgbColor EndingColor; 25 | }; 26 | 27 | static MyAnimationState animationState[1]; 28 | 29 | static void BlendAnimUpdate(const AnimationParam ¶m) 30 | { 31 | RgbColor updatedColor = RgbColor::LinearBlend( 32 | animationState[param.index].StartingColor, 33 | animationState[param.index].EndingColor, 34 | param.progress); 35 | 36 | strip.SetPixelColor(0, updatedColor); 37 | } 38 | 39 | static void startConfirmationFadeIn() 40 | { 41 | animationState[0].StartingColor = strip.GetPixelColor(0); 42 | animationState[0].EndingColor = RgbColor(4, 0, 153); 43 | animations.StartAnimation(0, 1000, BlendAnimUpdate); // 1 second fade in 44 | } 45 | 46 | static void startConfirmationFadeOut() 47 | { 48 | animationState[0].StartingColor = strip.GetPixelColor(0); 49 | animationState[0].EndingColor = RgbColor(0); 50 | animations.StartAnimation(0, 1000, BlendAnimUpdate); // 1 second fade out 51 | } 52 | 53 | static void handleConfirmationFlash() 54 | { 55 | unsigned long now = millis(); 56 | if (!animations.IsAnimating() && (now - lastFlashTime >= 2000)) 57 | { // 2 second interval 58 | flashState = !flashState; 59 | lastFlashTime = now; 60 | 61 | if (flashState) 62 | { 63 | startConfirmationFadeIn(); 64 | } 65 | else 66 | { 67 | startConfirmationFadeOut(); 68 | } 69 | } 70 | 71 | if (animations.IsAnimating()) 72 | { 73 | animations.UpdateAnimations(); 74 | strip.Show(); 75 | } 76 | } 77 | 78 | uint8_t splashscreenColorIndex = 0; 79 | 80 | static void handleQuickAcknowledgementFlash() 81 | { 82 | unsigned long now = millis(); 83 | if (!animations.IsAnimating()) 84 | { 85 | if (!flashState) 86 | { 87 | // Start the flash 88 | flashState = true; 89 | lastFlashTime = now; 90 | 91 | // Use different colors based on input source 92 | if (isEncoderTriggeredFlash) 93 | { 94 | animationState[0].StartingColor = RgbColor(179, 120, 20); 95 | } 96 | else 97 | { 98 | animationState[0].StartingColor = RgbColor(53, 105, 19); 99 | } 100 | animationState[0].EndingColor = strip.GetPixelColor(0); 101 | 102 | animations.StartAnimation(0, isEncoderTriggeredFlash ? 100 : 300, BlendAnimUpdate); 103 | } 104 | else 105 | { 106 | // Flash animation completed, reset everything 107 | flashState = false; 108 | 109 | setLedMode(lastMode); 110 | return; 111 | } 112 | } 113 | 114 | // Update animation if it's still running 115 | if (animations.IsAnimating()) 116 | { 117 | animations.UpdateAnimations(); 118 | strip.Show(); 119 | } 120 | } 121 | 122 | const RgbColor splashscreenColors[] = { 123 | RgbColor(0), 124 | RgbColor(48, 48, 48), 125 | RgbColor(32, 32, 32), 126 | RgbColor(0)}; 127 | 128 | static void handleSplashscreen() 129 | { 130 | if (!animations.IsAnimating()) 131 | { 132 | switch (splashscreenColorIndex) 133 | { 134 | case 0: 135 | animationState[0].StartingColor = splashscreenColors[0]; 136 | animationState[0].EndingColor = splashscreenColors[1]; 137 | animations.StartAnimation(0, 6000, BlendAnimUpdate); 138 | break; 139 | 140 | case 1: 141 | animationState[0].StartingColor = splashscreenColors[1]; 142 | animationState[0].EndingColor = splashscreenColors[2]; 143 | animations.StartAnimation(0, 4000, BlendAnimUpdate); 144 | break; 145 | 146 | case 2: 147 | animationState[0].StartingColor = splashscreenColors[2]; 148 | animationState[0].EndingColor = splashscreenColors[3]; 149 | animations.StartAnimation(0, 6000, BlendAnimUpdate); 150 | break; 151 | 152 | case 3: 153 | animationState[0].StartingColor = strip.GetPixelColor(0); 154 | animationState[0].EndingColor = RgbColor(0); 155 | animations.StartAnimation(0, 4000, BlendAnimUpdate); 156 | break; 157 | } 158 | 159 | splashscreenColorIndex++; 160 | if (splashscreenColorIndex > 3) 161 | { 162 | splashscreenColorIndex = 0; 163 | } 164 | } 165 | 166 | if (animations.IsAnimating()) 167 | { 168 | animations.UpdateAnimations(); 169 | strip.Show(); 170 | } 171 | } 172 | 173 | // track last time where we showed a quick acknowledgement flash 174 | // so that we dont trigger on the same button press multiple times 175 | static unsigned long lastQuickAcknowledgeFlashTime = 0; 176 | 177 | int lastEncoderCount = 0; 178 | volatile int *encoderCount; 179 | 180 | void ledSetupEncoder(volatile int *encoder) 181 | { 182 | encoderCount = encoder; 183 | lastEncoderCount = *encoder; 184 | } 185 | 186 | static void ledTask(void *parameter) 187 | { 188 | while (1) 189 | { 190 | // Show quick acknowledgement flash if button was pressed or the encoder was turned 191 | if ( 192 | (currentMode != LedMode::QuickAcknowledgementFlash) && 193 | (currentMode != LedMode::TimerPaused) && 194 | lastEncoderCount != *encoderCount) 195 | { 196 | Serial.println("=== Led: Quick acknowledgement flash (encoder)"); 197 | isEncoderTriggeredFlash = true; 198 | setLedMode(LedMode::QuickAcknowledgementFlash); 199 | lastQuickAcknowledgeFlashTime = Button::instance->lastPressTime; 200 | } 201 | else if (Button::instance && 202 | (currentMode != LedMode::QuickAcknowledgementFlash) && 203 | (currentMode != LedMode::TimerPaused) && 204 | (Button::instance->lastPressTime > lastQuickAcknowledgeFlashTime)) // use time in case we miss a press due to the task 205 | { 206 | Serial.println("=== Led: Quick acknowledgement flash"); 207 | isEncoderTriggeredFlash = false; 208 | setLedMode(LedMode::QuickAcknowledgementFlash); 209 | lastQuickAcknowledgeFlashTime = Button::instance->lastPressTime; 210 | } 211 | 212 | lastEncoderCount = *encoderCount; 213 | 214 | switch (currentMode) 215 | { 216 | case LedMode::Off: 217 | // if (strip.GetPixelColor(0) != RgbColor(0) && !animations.IsAnimating()) 218 | // { 219 | // animationState[0].StartingColor = strip.GetPixelColor(0); 220 | // animationState[0].EndingColor = RgbColor(0); 221 | // animations.StartAnimation(0, 300, BlendAnimUpdate); 222 | // } 223 | 224 | // if (animations.IsAnimating()) 225 | // { 226 | // animations.UpdateAnimations(); 227 | // strip.Show(); 228 | // } 229 | 230 | if (strip.GetPixelColor(0) != RgbColor(0)) 231 | { 232 | strip.SetPixelColor(0, RgbColor(0)); 233 | strip.Show(); 234 | lastMode = LedMode::Off; 235 | } 236 | break; 237 | 238 | case LedMode::QuickAcknowledgementFlash: 239 | handleQuickAcknowledgementFlash(); 240 | break; 241 | 242 | case LedMode::Splashscreen: 243 | handleSplashscreen(); 244 | break; 245 | 246 | case LedMode::ConfirmationFlash: 247 | handleConfirmationFlash(); 248 | break; 249 | 250 | case LedMode::TimerPaused: 251 | if (strip.GetPixelColor(0) != RgbColor(38 / 2, 99 / 2, 143 / 2)) 252 | { 253 | strip.SetPixelColor(0, RgbColor(38 / 2, 99 / 2, 143 / 2)); // Dim blue 254 | strip.Show(); 255 | } 256 | } 257 | // Small delay to prevent task from hogging CPU 258 | vTaskDelay(pdMS_TO_TICKS(10)); 259 | } 260 | } 261 | 262 | void setLedMode(LedMode mode) 263 | { 264 | Serial.printf("=== Led: Setting mode from %d to %d\n", currentMode, mode); 265 | if (currentMode != LedMode::QuickAcknowledgementFlash) 266 | { 267 | Serial.printf("=== Led: setting last mode to %d\n", currentMode); 268 | lastMode = currentMode; 269 | } 270 | 271 | currentMode = mode; 272 | // Reset animation state when mode changes 273 | // ignore Animations in non-zero index 274 | animations.StopAll(); 275 | 276 | // strip.SetPixelColor(0, RgbColor(0)); 277 | // strip.Show(); 278 | lastFlashTime = 0; 279 | flashState = false; 280 | } 281 | 282 | void setupLed() 283 | { 284 | strip.Begin(); 285 | strip.SetPixelColor(0, RgbColor(0)); 286 | strip.Show(); 287 | 288 | startLedTask(); 289 | } 290 | 291 | void stopLed() 292 | { 293 | if (ledTaskHandle != NULL) 294 | { 295 | vTaskDelete(ledTaskHandle); 296 | ledTaskHandle = NULL; 297 | } 298 | } 299 | 300 | void startLedTask() 301 | { 302 | xTaskCreatePinnedToCore( 303 | ledTask, // Task function 304 | "LED Task", // Name 305 | LED_TASK_STACK_SIZE, // Stack size 306 | NULL, // Parameters 307 | LED_TASK_PRIORITY, // Priority 308 | &ledTaskHandle, // Task handle 309 | LED_CORE // Core ID 310 | ); 311 | } -------------------------------------------------------------------------------- /src/led.h: -------------------------------------------------------------------------------- 1 | #ifndef LED_H 2 | #define LED_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | enum class LedMode 10 | { 11 | Off, 12 | Splashscreen, 13 | QuickAcknowledgementFlash, 14 | ConfirmationFlash, 15 | TimerPaused, 16 | }; 17 | 18 | void setupLed(); 19 | void ledSetupEncoder(volatile int *encoderCount); 20 | void stopLed(); 21 | void startLedTask(); 22 | void setLedMode(LedMode mode); 23 | 24 | extern TaskHandle_t ledTaskHandle; 25 | 26 | #endif -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "GxEPD2_display_selection_new_style.h" 9 | #include "timer.h" 10 | #include "led.h" 11 | #include "debug.h" 12 | #include "images.h" 13 | #include "checkbox.h" 14 | #include "splashscreen.h" 15 | #include "button.h" 16 | #include "icon_provider.h" 17 | #include "anniversary.h" 18 | #include "preferences_manager.h" 19 | 20 | #if STRINGS_TEST 21 | #include 22 | #endif 23 | 24 | #define MINUTE 60 * 1000 25 | 26 | #define ENCODER_CLK 32 27 | #define ENCODER_DT 21 28 | 29 | #define ENCODER_STABILITY_DELAY 50 // ms to wait for stable position 30 | #define ENCODER_LOCK_TIME_AFTER_BUTTON 200 // ms to ignore encoder changes after button press 31 | 32 | ESP32Encoder encoder; 33 | volatile unsigned long lastEncoderUpdate = 0; 34 | const unsigned long encoderDebounceTime = 10; // ms 35 | volatile int debouncedCount = 0; 36 | volatile int lastCount = 0; 37 | volatile int tempCount = 0; 38 | volatile unsigned long tempCountTime = 0; 39 | volatile bool positionStable = true; 40 | volatile bool buttonPressed = false; 41 | volatile unsigned long lastButtonPressTime = 0; 42 | 43 | #if CHECKBOX_TEST 44 | Checkbox checkbox(&icon_lpehacker, "A test", "test"); 45 | Checkbox checkbox2(&icon_lpetantrum, "Another test", "test2"); 46 | #endif 47 | 48 | void IRAM_ATTR checkPosition(void *arg) 49 | { 50 | unsigned long currentTime = millis(); 51 | int currentCount = encoder.getCount(); 52 | 53 | // If there was a recent button press, ignore encoder changes 54 | if (currentTime - Button::instance->lastPressTime < ENCODER_STABILITY_DELAY) 55 | { 56 | return; 57 | } 58 | 59 | if (currentCount % 2 == 0 && currentCount != debouncedCount && (currentTime - lastEncoderUpdate >= encoderDebounceTime)) 60 | { 61 | debouncedCount = (currentCount % 2 == 0 ? currentCount : currentCount - 1) / 2; 62 | lastEncoderUpdate = currentTime; 63 | } 64 | } 65 | 66 | void setupEncoder() 67 | { 68 | pinMode(ENCODER_CLK, INPUT); 69 | pinMode(ENCODER_DT, INPUT); 70 | 71 | encoder = ESP32Encoder(true, checkPosition); 72 | ESP32Encoder::useInternalWeakPullResistors = puType::none; 73 | 74 | encoder.attachHalfQuad(ENCODER_DT, ENCODER_CLK); 75 | encoder.setFilter(1023); 76 | 77 | encoder.clearCount(); 78 | debouncedCount = 0; 79 | lastCount = 0; 80 | } 81 | 82 | Timer timer(display); 83 | 84 | void setup() 85 | { 86 | pinMode(2, OUTPUT); 87 | digitalWrite(2, HIGH); 88 | 89 | Button::instance = new Button(ENCODER_SW); 90 | ledSetupEncoder(&debouncedCount); 91 | 92 | setupLed(); 93 | 94 | Serial.begin(115200); 95 | 96 | // Initialize the encoder 97 | setupEncoder(); 98 | 99 | // Initialize preferences once 100 | initPreferences(); 101 | 102 | // Initialize the display 103 | display.init(115200, true, 2, false); 104 | display.setRotation(0); 105 | 106 | // Load LPE mode setting from checkbox 107 | IconProvider::getInstance()->setLpeMode(pref_getCheckbox("lpe", true)); 108 | 109 | #ifdef DEBUG 110 | #if ICON_SCALING_TEST 111 | const uint16_t padding = 20; 112 | 113 | const auto sizes = {48, 64, 128, 192}; 114 | uint16_t xOffset = padding; 115 | for (const auto size : sizes) 116 | { 117 | const auto icon = icon_lpesip.scaled(size); 118 | const uint16_t yOffset = padding + 192 - size; 119 | // draw each icon size side by side left to right 120 | display.drawBitmap(xOffset, padding, icon.data, size, size, GxEPD_BLACK); 121 | drawCenteredText(display, String(size).c_str(), xOffset + size / 2, size + 2 * padding, &SUB_FONT, GxEPD_BLACK); 122 | 123 | drawCenteredText(display, "#autoscaling", display.width() / 2, display.height() / 2 + 48, &MAIN_FONT, GxEPD_BLACK); 124 | 125 | xOffset += size + padding; 126 | } 127 | 128 | display.display(); 129 | 130 | while (true) 131 | ; 132 | #endif 133 | 134 | #if PATTERN_TEST 135 | 136 | // draw each pattern in a rounded area on a grid within display bounds 137 | const uint16_t padding = 10; 138 | const uint16_t w = display.width() / patterns.size() - 2 * padding; 139 | for (uint16_t i = 0; i < patterns.size(); i++) 140 | { 141 | drawPatternInRoundedArea(display, i * display.width() / patterns.size() + padding, padding, w, display.height() - 2 * padding, 10, Pattern(i)); 142 | 143 | char buffer[2]; 144 | sprintf(buffer, "%d", i); 145 | drawCenteredText(display, buffer, i * display.width() / patterns.size() + display.width() / patterns.size() / 2, display.height() - padding - 10, &SUB_FONT, GxEPD_BLACK); 146 | } 147 | 148 | display.display(); 149 | 150 | while (true) 151 | ; 152 | 153 | #endif 154 | 155 | #if IMAGE_CYCLE_TEST 156 | std::vector images = { 157 | // image_bg_cat, 158 | image_bg_stonks, 159 | image_bg_pablo, 160 | image_bg_what_a_week, 161 | }; 162 | 163 | display.fillScreen(GxEPD_WHITE); 164 | display.display(); 165 | 166 | for (const auto image : images) 167 | { 168 | 169 | display.fillScreen(GxEPD_WHITE); 170 | display.drawBitmap(0, 0, image, display.width(), display.height(), GxEPD_BLACK); 171 | display.display(false); 172 | display.drawBitmap(0, 0, image, display.width(), display.height(), GxEPD_BLACK); 173 | display.display(true); 174 | display.drawBitmap(0, 0, image, display.width(), display.height(), GxEPD_BLACK); 175 | display.display(true); 176 | 177 | delay(5000); 178 | } 179 | #endif 180 | 181 | #if CHECKBOX_TEST 182 | display.fillScreen(GxEPD_WHITE); 183 | 184 | Checkbox *selected = &checkbox; 185 | 186 | checkbox.load(); 187 | checkbox2.load(); 188 | 189 | checkbox.draw(display, 0, 0, display.width(), 96, selected == &checkbox); 190 | checkbox2.draw(display, 0, 96 + 8, display.width(), 96, selected == &checkbox2); 191 | display.display(); 192 | while (true) 193 | { 194 | if (!digitalRead(ENCODER_SW)) 195 | { 196 | display.fillScreen(GxEPD_WHITE); 197 | 198 | selected->toggle(); 199 | selected->save(); 200 | 201 | checkbox.draw(display, 0, 0, display.width(), 96, selected == &checkbox); 202 | checkbox2.draw(display, 0, 96 + 8, display.width(), 96, selected == &checkbox2); 203 | display.display(); 204 | delay(500); 205 | } 206 | 207 | if (debouncedCount != lastCount) 208 | { 209 | display.fillScreen(GxEPD_WHITE); 210 | 211 | selected->draw(display, 0, selected == &checkbox ? 0 : 96 + 8, display.width(), 96, false); 212 | selected = selected == &checkbox ? &checkbox2 : &checkbox; 213 | selected->draw(display, 0, selected == &checkbox ? 0 : 96 + 8, display.width(), 96, true); 214 | 215 | display.display(); 216 | } 217 | 218 | lastCount = debouncedCount; 219 | } 220 | #endif 221 | 222 | #endif 223 | 224 | #if STRINGS_TEST 225 | 226 | // Space for message: 221,330 until 649,409 227 | const uint16_t messageMinX = 221; 228 | const uint16_t messageMaxX = 649; 229 | const uint16_t messageMinY = 330; 230 | const uint16_t messageMaxY = 409; 231 | const uint16_t messageW = messageMaxX - messageMinX; 232 | const uint16_t messageH = messageMaxY - messageMinY; 233 | 234 | Serial.println("--- Messages ---"); 235 | 236 | for (auto msg : messageCache.getMessages()) 237 | { 238 | // Split message by lines and print individually 239 | std::string messageStr(msg); 240 | std::istringstream iss(messageStr); 241 | std::string line; 242 | int lineIndex = 0; 243 | while (std::getline(iss, line, '\n')) 244 | { 245 | if (lineIndex >= 3) 246 | { 247 | Serial.printf("ERR: Too many lines in message \"%s\"\n", msg); 248 | break; 249 | } 250 | 251 | int yPos = messageMinY + lineIndex * 18 + 18; // + 18 because of the first line 252 | Bounds b = drawText(display, line.c_str(), messageMinX, yPos, &SMALL_FONT, GxEPD_BLACK); 253 | ++lineIndex; 254 | 255 | if (b.w > messageW || b.y + b.h > messageMaxY) 256 | { 257 | auto wOver = b.w - messageW; 258 | auto hOver = b.y + b.h - messageMaxY; 259 | 260 | Serial.printf("ERR: Line too long: \"%s\" {x: %d, y: %d, w: %d (+%d), h: %d (+%d)}\n", line.c_str(), b.x, b.y, b.w, wOver > 0 ? wOver : 0, b.h, hOver > 0 ? hOver : 0); 261 | } 262 | else 263 | { 264 | Serial.printf("OK: \"%s\" {x: %d, y: %d, w: %d, h: %d}\n", line.c_str(), b.x, b.y, b.w, b.h); 265 | } 266 | } 267 | } 268 | #endif 269 | 270 | #if ANNIVERSARY_MODE 271 | if (!pref_getCheckbox("anniversary", false)) 272 | { 273 | Anniversary anniversary(display); 274 | 275 | anniversary.loop(); 276 | 277 | pref_putCheckbox("anniversary", true); 278 | } 279 | #endif 280 | 281 | setLedMode(LedMode::Splashscreen); 282 | 283 | SplashScreen splashScreen(display, timer); 284 | splashScreen.draw(); 285 | splashScreen.loop(&debouncedCount); 286 | setLedMode(LedMode::Off); 287 | 288 | auto iconProvider = IconProvider::getInstance(); 289 | 290 | timer.addPreset(iconProvider->getPresetIcon("Emails"), iconProvider->getTimerRunningBackgroundImage(), "Emails", 15 * MINUTE, 5 * MINUTE, 15 * MINUTE); 291 | timer.addPreset(iconProvider->getPresetIcon("Coding"), iconProvider->getTimerRunningBackgroundImage(), "Coding", 45 * MINUTE, 15 * MINUTE, 30 * MINUTE, 2); 292 | timer.addPreset(iconProvider->getPresetIcon("Focus"), iconProvider->getTimerRunningBackgroundImage(), "Focus", 25 * MINUTE, 5 * MINUTE, 20 * MINUTE); 293 | 294 | timer.selectPreset(1); 295 | } 296 | 297 | void loop() 298 | { 299 | timer.loop(&debouncedCount); 300 | lastCount = debouncedCount; 301 | } 302 | -------------------------------------------------------------------------------- /src/menu.cpp: -------------------------------------------------------------------------------- 1 | #include "menu.h" 2 | 3 | Menu::Menu(DISPLAY_CLASS &display, MenuItem *items, int itemCount) : display(display) // Update constructor 4 | { 5 | this->items = items; 6 | this->itemCount = itemCount; 7 | this->selectedIndex = 0; 8 | } 9 | 10 | Menu::~Menu() 11 | { 12 | } 13 | 14 | MenuItem *Menu::getSelected() 15 | { 16 | return &items[selectedIndex]; 17 | } 18 | 19 | MenuItem *Menu::getItems() 20 | { 21 | return items; 22 | } 23 | 24 | int Menu::getSelectedIndex() 25 | { 26 | return selectedIndex; 27 | } 28 | 29 | int Menu::getItemCount() 30 | { 31 | return itemCount; 32 | } 33 | 34 | void Menu::setSelectedIndex(int index) 35 | { 36 | selectedIndex = index; 37 | } 38 | 39 | void Menu::setEncoderCount(int encoderCount) 40 | { 41 | lastEncoderCount = encoderCount; 42 | } 43 | 44 | void Menu::next() 45 | { 46 | selectedIndex = (selectedIndex + 1) % itemCount; 47 | } 48 | 49 | void Menu::previous() 50 | { 51 | selectedIndex = (selectedIndex - 1 + itemCount) % itemCount; 52 | } 53 | 54 | bool Menu::loop(volatile int *encoderCount) 55 | { 56 | if (*encoderCount != lastEncoderCount) 57 | { 58 | if (*encoderCount < lastEncoderCount) 59 | { 60 | previous(); 61 | } 62 | else 63 | { 64 | next(); 65 | } 66 | 67 | lastEncoderCount = *encoderCount; 68 | return true; 69 | } 70 | 71 | return false; 72 | } 73 | 74 | MenuItem::MenuItem(const char *text, Icon *icon) : text(text), icon(icon) 75 | { 76 | } 77 | 78 | MenuItem::~MenuItem() 79 | { 80 | } 81 | 82 | const char *MenuItem::getText() 83 | { 84 | return text; 85 | } 86 | 87 | void MenuItem::setText(const char *text) 88 | { 89 | this->text = text; 90 | } 91 | 92 | Icon *MenuItem::getIcon() 93 | { 94 | return icon; 95 | } 96 | -------------------------------------------------------------------------------- /src/menu.h: -------------------------------------------------------------------------------- 1 | #ifndef MENU_H 2 | #define MENU_H 3 | 4 | #include "defs.h" 5 | #include 6 | #include 7 | #include 8 | #include "icon.h" 9 | #include "debug.h" 10 | 11 | enum class DrawStyle 12 | { 13 | Vertical, 14 | Horizontal 15 | }; 16 | 17 | class MenuItem 18 | { 19 | private: 20 | const char *text; 21 | Icon *icon; 22 | 23 | public: 24 | MenuItem(const char *text, Icon *icon = nullptr); 25 | ~MenuItem(); 26 | 27 | const char *getText(); 28 | void setText(const char *text); 29 | Icon *getIcon(); 30 | }; 31 | 32 | class Menu 33 | { 34 | private: 35 | DISPLAY_CLASS &display; // Update display type 36 | MenuItem *items; 37 | int itemCount; 38 | int selectedIndex; 39 | 40 | int lastEncoderCount = 0; 41 | 42 | public: 43 | Menu(DISPLAY_CLASS &display, MenuItem *items, int itemCount); // Update constructor 44 | ~Menu(); 45 | 46 | MenuItem *getSelected(); 47 | MenuItem *getItems(); 48 | int getSelectedIndex(); 49 | int getItemCount(); 50 | 51 | void setSelectedIndex(int index); 52 | void setEncoderCount(int encoderCount); 53 | 54 | void next(); 55 | void previous(); 56 | 57 | bool loop(volatile int *encoderCount); 58 | }; 59 | 60 | #endif // MENU_H 61 | -------------------------------------------------------------------------------- /src/preferences_manager.cpp: -------------------------------------------------------------------------------- 1 | #include "preferences_manager.h" 2 | 3 | Preferences preferences; 4 | 5 | void initPreferences() 6 | { 7 | // preferences.begin(PREFS_NAMESPACE, false); 8 | } 9 | 10 | bool pref_getCheckbox(const char *key, bool defaultValue) 11 | { 12 | preferences.begin(PREFS_NAMESPACE, true); 13 | char prefKey[32]; 14 | snprintf(prefKey, sizeof(prefKey), "%s%s", PREF_CHECKBOX, key); 15 | 16 | auto v = preferences.getBool(prefKey, defaultValue); 17 | 18 | preferences.end(); 19 | 20 | return v; 21 | } 22 | 23 | void pref_putCheckbox(const char *key, bool value) 24 | { 25 | preferences.begin(PREFS_NAMESPACE, false); 26 | char prefKey[32]; 27 | snprintf(prefKey, sizeof(prefKey), "%s%s", PREF_CHECKBOX, key); 28 | preferences.putBool(prefKey, value); 29 | preferences.end(); 30 | } 31 | 32 | unsigned int pref_getStatistic(const char *key, unsigned int defaultValue) 33 | { 34 | preferences.begin(PREFS_NAMESPACE, true); 35 | char prefKey[32]; 36 | snprintf(prefKey, sizeof(prefKey), "%s%s", PREF_STATISTICS, key); 37 | 38 | auto v = preferences.getUInt(prefKey, defaultValue); 39 | 40 | preferences.end(); 41 | return v; 42 | } 43 | 44 | void pref_putStatistic(const char *key, unsigned int value) 45 | { 46 | preferences.begin(PREFS_NAMESPACE, false); 47 | char prefKey[32]; 48 | snprintf(prefKey, sizeof(prefKey), "%s%s", PREF_STATISTICS, key); 49 | preferences.putUInt(prefKey, value); 50 | preferences.end(); 51 | } 52 | 53 | unsigned long pref_getStatistic(const char *key, unsigned long defaultValue) 54 | { 55 | preferences.begin(PREFS_NAMESPACE, true); 56 | char prefKey[32]; 57 | snprintf(prefKey, sizeof(prefKey), "%s%s", PREF_STATISTICS, key); 58 | 59 | auto v = preferences.getULong(prefKey, defaultValue); 60 | 61 | preferences.end(); 62 | return v; 63 | } 64 | 65 | void pref_putStatistic(const char *key, unsigned long value) 66 | { 67 | preferences.begin(PREFS_NAMESPACE, false); 68 | char prefKey[32]; 69 | snprintf(prefKey, sizeof(prefKey), "%s%s", PREF_STATISTICS, key); 70 | preferences.putULong(prefKey, value); 71 | preferences.end(); 72 | } -------------------------------------------------------------------------------- /src/preferences_manager.h: -------------------------------------------------------------------------------- 1 | #ifndef PREFERENCES_MANAGER_H 2 | #define PREFERENCES_MANAGER_H 3 | 4 | #include 5 | 6 | // Global preferences namespace 7 | #define PREFS_NAMESPACE "pomodoro" 8 | 9 | // Preferences keys prefixes for different parts of the application 10 | #define PREF_CHECKBOX "cb." 11 | #define PREF_STATISTICS "stats." 12 | #define PREF_ANNIVERSARY "anniv." 13 | 14 | extern Preferences preferences; 15 | 16 | void initPreferences(); 17 | 18 | bool pref_getCheckbox(const char *key, bool defaultValue); 19 | void pref_putCheckbox(const char *key, bool value); 20 | 21 | unsigned int pref_getStatistic(const char *key, unsigned int defaultValue); 22 | void pref_putStatistic(const char *key, unsigned int value); 23 | 24 | unsigned long pref_getStatistic(const char *key, unsigned long defaultValue); 25 | void pref_putStatistic(const char *key, unsigned long value); 26 | 27 | #endif -------------------------------------------------------------------------------- /src/splashscreen.cpp: -------------------------------------------------------------------------------- 1 | #include "splashscreen.h" 2 | #include "button.h" 3 | #include "icon_provider.h" 4 | 5 | SplashScreen::SplashScreen(DISPLAY_CLASS &display, Timer &timer) : display(display), timer(timer) 6 | { 7 | for (int i = 0; i < checkboxes.size(); i++) 8 | { 9 | checkboxes[i].load(); 10 | } 11 | 12 | selectedSettingsIndex = 0; 13 | selectedCheckbox = &checkboxes[selectedSettingsIndex]; 14 | } 15 | 16 | SplashScreen::~SplashScreen() 17 | { 18 | } 19 | 20 | void SplashScreen::draw() 21 | { 22 | display.fillScreen(GxEPD_WHITE); 23 | 24 | display.drawBitmap(0, 0, image_bg_splash, display.width(), display.height(), GxEPD_BLACK); 25 | 26 | const uint16_t padding = 20; 27 | const uint16_t menuY = padding; 28 | const uint16_t paddingBetweenBoxes = 20; 29 | const uint16_t menuItemCount = buttons.getItemCount(); 30 | const uint16_t menuItemWidth = (display.width() - paddingBetweenBoxes * (menuItemCount - 1) - padding * 2) / menuItemCount; 31 | const uint16_t menuHeight = 48; 32 | 33 | for (int i = 0; i < menuItemCount; i++) 34 | { 35 | const bool selected = i == buttons.getSelectedIndex(); 36 | auto item = buttons.getItems()[i]; 37 | const uint16_t xOffset = padding + i * (menuItemWidth + paddingBetweenBoxes); 38 | 39 | if (selected) 40 | { 41 | display.fillRoundRect(xOffset, menuY, menuItemWidth, menuHeight, 10, GxEPD_WHITE); 42 | drawPatternInRoundedArea(display, xOffset, menuY, menuItemWidth, menuHeight, 10, Pattern::SparseDots); 43 | } 44 | else 45 | { 46 | display.fillRoundRect(xOffset, menuY, menuItemWidth, menuHeight, 10, GxEPD_WHITE); 47 | } 48 | display.drawRoundRect(xOffset, menuY, menuItemWidth, menuHeight, 10, GxEPD_BLACK); 49 | drawCenteredText(display, item.getText(), xOffset + menuItemWidth / 2, menuY + menuHeight / 2, &SUB_FONT, GxEPD_BLACK); 50 | } 51 | 52 | display.display(true); 53 | } 54 | 55 | void SplashScreen::loop(volatile int *encoderCount) 56 | { 57 | while (true) 58 | { 59 | if ( 60 | buttons.loop(encoderCount)) 61 | { 62 | // Redraw 63 | draw(); 64 | lastEncoderCount = *encoderCount; 65 | } 66 | 67 | if (!Button::instance) 68 | { 69 | Serial.println("Button instance is null"); 70 | continue; 71 | } 72 | 73 | if (Button::instance->checkAndClearButtonPress()) 74 | { 75 | if (buttons.getSelectedIndex() == 0) 76 | { 77 | timer.start(); 78 | return; 79 | } 80 | else if (buttons.getSelectedIndex() == 1) 81 | { 82 | display.fillScreen(GxEPD_WHITE); 83 | drawSettings(); 84 | loopSettings(encoderCount); 85 | } 86 | return; 87 | } 88 | } 89 | } 90 | 91 | void SplashScreen::drawSettings() 92 | { 93 | display.fillScreen(GxEPD_WHITE); 94 | 95 | for (int i = 0; i < checkboxes.size(); i++) 96 | { 97 | auto checkbox = checkboxes[i]; 98 | const bool selected = checkbox.getName() == selectedCheckbox->getName(); 99 | 100 | checkbox.draw(display, 16, i * 96 + 16 + i * 32, display.width() - 32, 96, selected); 101 | } 102 | 103 | display.display(true); 104 | } 105 | 106 | void SplashScreen::loopSettings(volatile int *encoderCount) 107 | { 108 | while (true) 109 | { 110 | if (lastEncoderCount != *encoderCount) 111 | { 112 | // select next or previous checkbox 113 | if (*encoderCount < lastEncoderCount) 114 | { 115 | selectedSettingsIndex = (selectedSettingsIndex - 1 + checkboxes.size()) % checkboxes.size(); 116 | selectedCheckbox = &checkboxes[selectedSettingsIndex]; 117 | } 118 | else 119 | { 120 | selectedSettingsIndex = (selectedSettingsIndex + 1) % checkboxes.size(); 121 | selectedCheckbox = &checkboxes[selectedSettingsIndex]; 122 | } 123 | 124 | // Redraw 125 | drawSettings(); 126 | 127 | lastEncoderCount = *encoderCount; 128 | } 129 | 130 | if (Button::instance->checkAndClearButtonPress()) 131 | { 132 | if (selectedCheckbox->getName() == "Reset Device") 133 | { 134 | Preferences preferences; 135 | preferences.begin("pomodoro", false); 136 | preferences.clear(); 137 | preferences.end(); 138 | 139 | display.fillScreen(GxEPD_BLACK); 140 | display.display(); 141 | 142 | delay(5000); 143 | ESP.restart(); 144 | } 145 | 146 | // save settings 147 | selectedCheckbox->toggle(); 148 | selectedCheckbox->save(); 149 | 150 | // Redraw 151 | drawSettings(); 152 | 153 | delay(1000); 154 | 155 | ESP.restart(); 156 | } 157 | } 158 | } -------------------------------------------------------------------------------- /src/splashscreen.h: -------------------------------------------------------------------------------- 1 | #ifndef SPLASHSCREEN_H 2 | #define SPLASHSCREEN_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include "gfx_utils.h" 8 | #include "icons.h" 9 | #include "icon.h" 10 | #include "timer.h" 11 | #include "checkbox.h" 12 | #include "debug.h" 13 | #include "images.h" 14 | #include 15 | 16 | class SplashScreen 17 | { 18 | private: 19 | DISPLAY_CLASS &display; 20 | Timer &timer; 21 | 22 | Menu buttons = Menu(display, new MenuItem[2]{MenuItem("Start"), MenuItem("Einstellungen")}, 2); 23 | 24 | std::vector checkboxes = { 25 | Checkbox(&icon_lpehacker, "LPE Modus", "lpe", true), 26 | Checkbox(&icon_lpenote, "Ablenkende Nachrichten", "msgs", true), 27 | Checkbox(nullptr, "Reset Device", "reset"), 28 | }; 29 | 30 | Checkbox *selectedCheckbox; 31 | int16_t selectedSettingsIndex = 0; 32 | int16_t lastEncoderCount = 0; 33 | 34 | public: 35 | SplashScreen(DISPLAY_CLASS &display, Timer &timer); 36 | ~SplashScreen(); 37 | void setLastEncoderCount(int16_t count); 38 | void draw(); 39 | void loop(volatile int *encoderCount); 40 | 41 | void drawSettings(); 42 | void loopSettings(volatile int *encoderCount); 43 | }; 44 | 45 | #endif -------------------------------------------------------------------------------- /src/states/timer_running.cpp: -------------------------------------------------------------------------------- 1 | #include "../timer.h" 2 | #include "../strings.h" 3 | #include 4 | 5 | void Timer::handleRunning(volatile int *encoderCount) 6 | { 7 | if (state == TimerState::Running) 8 | { 9 | elapsed = millis() - startTime - totalPausedTime; 10 | } 11 | 12 | if (state == TimerState::Running && millis() - lastMessageUpdate >= RUNNING_MESSAGE_REFRESH_INTERVAL) 13 | { 14 | lastMessageUpdate = millis(); 15 | messageCache.clearCache(Messages::Preset_Email_Message); 16 | messageCache.clearCache(Messages::Preset_Coding_Message); 17 | messageCache.clearCache(Messages::Preset_Focus_Message); 18 | 19 | needsRedraw = true; 20 | } 21 | 22 | if (state == TimerState::Running && elapsed >= currentPreset->getDuration()) 23 | { 24 | 25 | longestEarnedPauseInShortCycles = max(currentPreset->getLongPauseDuration(), longestEarnedPauseInShortCycles); 26 | 27 | setLedMode(LedMode::ConfirmationFlash); 28 | state = TimerState::WaitingConfirmStartOfBreak; 29 | 30 | this->confirmationMenu = new Menu(display, new MenuItem[1]{MenuItem(messageCache.getMessage(Messages::MenuItem_StartBreak))}, 1); 31 | this->confirmationMenu->setEncoderCount(*encoderCount); // Sync the encoder count 32 | needsFullRedraw = true; 33 | return; 34 | } 35 | 36 | // Only trigger redraw once per second to avoid unnecessary updates 37 | if (millis() - lastRedrawTime >= redrawInterval) 38 | { 39 | needsRedraw = true; 40 | } 41 | 42 | // Handle menu input 43 | if (topMenu) 44 | { 45 | if (Button::instance->checkAndClearButtonPress()) 46 | { 47 | // Handle menu selection 48 | if (topMenu->getSelectedIndex() == 0) 49 | { 50 | if (state == TimerState::Running) 51 | { 52 | // Pause 53 | pause(); 54 | topMenu->getSelected()->setText(messageCache.getMessage(Messages::MenuItem_Resume)); 55 | } 56 | else 57 | { 58 | // Resume 59 | resume(); 60 | topMenu->getSelected()->setText(messageCache.getMessage(Messages::MenuItem_Pause)); 61 | } 62 | 63 | needsRedraw = true; 64 | } 65 | else if (topMenu->getSelectedIndex() == 1) 66 | { 67 | // Break now 68 | startBreak(); 69 | } 70 | else if (topMenu->getSelectedIndex() == 2) 71 | { 72 | // Cancel 73 | incrementTotalTime(elapsed); 74 | minutesWorked += elapsed / 1000 / 60; 75 | stop(); 76 | enterPresetSelection(); 77 | 78 | needsFullRedraw = true; 79 | } 80 | } 81 | 82 | if (topMenu->loop(encoderCount)) 83 | { 84 | menuNeedsRedraw = true; 85 | } 86 | } 87 | } 88 | 89 | void Timer::drawRunning() 90 | { 91 | display.fillScreen(GxEPD_WHITE); 92 | 93 | if (showSpeechBubble) 94 | { 95 | display.drawBitmap(0, 0, currentPreset->getBackground(), display.width(), display.height(), GxEPD_BLACK); 96 | } 97 | 98 | drawMenuBar(); 99 | 100 | unsigned int remainingUnit = 0; 101 | char buffer[32]; // Increased buffer size to be safe 102 | 103 | const unsigned int remainingMillis = currentPreset->getDuration() - elapsed; 104 | const unsigned int seconds = remainingMillis / 1000; 105 | const unsigned int minutes = max(seconds / 60, 1u); 106 | uint16_t roundedSeconds = (seconds + 9) / 10 * 10; 107 | 108 | if (roundedSeconds >= 60) 109 | { 110 | sprintf(buffer, "%d %s", minutes, messageCache.getMessage(Messages::TimeFormat_Minutes)); 111 | } 112 | else 113 | { 114 | if (redrawInterval != REDRAW_INTERVAL_FAST) 115 | { 116 | redrawInterval = REDRAW_INTERVAL_FAST; 117 | needsRedraw = true; 118 | } 119 | 120 | // Round to nearest 10 seconds when below 1 minute 121 | sprintf(buffer, "%d %s", roundedSeconds, messageCache.getMessage(Messages::TimeFormat_Seconds)); 122 | } 123 | 124 | const uint16_t progressBarHeight = 32; 125 | 126 | Bounds boundsMin = getBounds(display, buffer, &LARGE_FONT); 127 | 128 | if (!showSpeechBubble) 129 | { 130 | // Draw text in the center 131 | drawText(display, buffer, display.width() / 2 - boundsMin.w / 2, display.height() / 2 + boundsMin.h / 2, &LARGE_FONT, GxEPD_BLACK); 132 | return; 133 | } 134 | 135 | drawText(display, buffer, display.width() / 2 - boundsMin.w / 2, display.height() / 3 + boundsMin.h / 2, &LARGE_FONT, GxEPD_BLACK); 136 | 137 | const unsigned int progress = (elapsed * 100) / currentPreset->getDuration(); 138 | const uint16_t progressBarWidth = display.width() / 2; 139 | 140 | drawProgressBar(display, ProgressBarStyle::Bordered, display.width() / 2 - progressBarWidth / 2, display.height() / 3 + boundsMin.h / 2 + 16, progressBarWidth, progressBarHeight, progressBarHeight / 2, progress); 141 | 142 | drawDebugCrosshair(display, display.width() / 2, display.height() / 2, 48); 143 | 144 | // Space for message: 221,330 until 649,409 145 | const uint16_t messageMinX = 221; 146 | const uint16_t messageMaxX = 649; 147 | const uint16_t messageMinY = 330; 148 | const uint16_t messageMaxY = 409; 149 | const uint16_t messageW = messageMaxX - messageMinX; 150 | const uint16_t messageH = messageMaxY - messageMinY; 151 | 152 | drawDebugCrosshair(display, 221, 330); 153 | 154 | // Split message by lines and print individually 155 | std::string messageStr(getRunningMessage()); 156 | std::istringstream iss(messageStr); 157 | std::string line; 158 | int lineIndex = 0; 159 | while (std::getline(iss, line, '\n')) 160 | { 161 | int yPos = messageMinY + lineIndex * 18 + 18 + lineIndex * 2; // + 18 because of the first line 162 | drawText(display, line.c_str(), messageMinX, yPos, &SMALL_FONT, GxEPD_BLACK); 163 | ++lineIndex; 164 | } 165 | } 166 | 167 | const char *Timer::getRunningMessage() 168 | { 169 | if (currentPreset == nullptr) 170 | { 171 | return "No preset selected"; 172 | } 173 | 174 | if (currentPreset->getName() == nullptr) 175 | { 176 | return "No name"; 177 | } 178 | 179 | if (strcmp(currentPreset->getName(), "Emails") == 0) 180 | { 181 | return messageCache.getMessage(Messages::Preset_Email_Message); 182 | } 183 | else if (strcmp(currentPreset->getName(), "Coding") == 0) 184 | { 185 | return messageCache.getMessage(Messages::Preset_Coding_Message); 186 | } 187 | else if (strcmp(currentPreset->getName(), "Focus") == 0) 188 | { 189 | return messageCache.getMessage(Messages::Preset_Focus_Message); 190 | } 191 | 192 | return "Unknown preset"; 193 | } -------------------------------------------------------------------------------- /src/states/timer_running_break.cpp: -------------------------------------------------------------------------------- 1 | #include "../timer.h" 2 | #include "../strings.h" 3 | 4 | void Timer::handleRunningBreak(volatile int *encoderCount) 5 | { 6 | if (state == TimerState::RunningBreak) 7 | { 8 | elapsed = millis() - startTime - totalPausedTime; 9 | } 10 | 11 | if (state == TimerState::RunningBreak && millis() - startTime >= currentBreakDuration) 12 | { 13 | setLedMode(LedMode::ConfirmationFlash); 14 | state = TimerState::WaitingConfirmEndOfBreak; 15 | 16 | this->confirmationMenu = new Menu(display, new MenuItem[2]{MenuItem(messageCache.getMessage(Messages::MenuItem_RestartTimer)), MenuItem(messageCache.getMessage(Messages::MenuItem_BackToPresets))}, 2); 17 | this->confirmationMenu->setEncoderCount(*encoderCount); // Sync encoder count 18 | needsFullRedraw = true; 19 | return; 20 | } 21 | 22 | // Handle menu input 23 | if (topMenu) 24 | { 25 | if (Button::instance->checkAndClearButtonPress()) 26 | { 27 | // Handle menu selection 28 | if (topMenu->getSelectedIndex() == 0) 29 | { 30 | if (state == TimerState::RunningBreak) 31 | { 32 | // Pause 33 | pause(); 34 | } 35 | else 36 | { 37 | // Resume 38 | resume(); 39 | } 40 | } 41 | else if (topMenu->getSelectedIndex() == 1) 42 | { 43 | // Skip break 44 | incrementTotalBreakTime(elapsed); 45 | minutesOnBreak += elapsed / 1000 / 60; 46 | reset(); 47 | enterPresetSelection(); 48 | } 49 | else if (topMenu->getSelectedIndex() == 2) 50 | { 51 | // Cancel 52 | incrementTotalBreakTime(elapsed); 53 | minutesOnBreak += elapsed / 1000 / 60; 54 | stop(); 55 | selectPreset(1); 56 | enterPresetSelection(); 57 | } 58 | 59 | needsFullRedraw = true; 60 | } 61 | 62 | if (topMenu->loop(encoderCount)) 63 | { 64 | menuNeedsRedraw = true; 65 | } 66 | } 67 | 68 | if (millis() - lastRedrawTime >= redrawInterval) 69 | { 70 | needsRedraw = true; 71 | } 72 | } 73 | 74 | String formatDuration(unsigned long minutes) 75 | { 76 | unsigned long hours = minutes / 60; 77 | minutes = minutes % 60; 78 | 79 | char buffer[32]; 80 | if (hours > 0) 81 | { 82 | sprintf(buffer, "%luh %lum", hours, minutes); 83 | } 84 | else 85 | { 86 | sprintf(buffer, "%lum", minutes); 87 | } 88 | 89 | return String(buffer); 90 | } 91 | 92 | void Timer::drawRunningBreak() 93 | { 94 | display.fillScreen(GxEPD_WHITE); 95 | 96 | if (isLongBreak) 97 | { 98 | display.drawBitmap(0, 8 + 48 + 4 + 8, breakImage, display.width(), display.height() - 8 - 48 - 4 - 8 - 64 - 4, GxEPD_BLACK); 99 | } 100 | else 101 | { 102 | const uint16_t w = 420; 103 | const uint16_t h = 340; 104 | Bounds boxBounds = {display.width() / 2 - w / 2, display.height() - h - 64, w, h}; 105 | const uint16_t innerPadding = 8; 106 | uint16_t yOffset = boxBounds.y + innerPadding; 107 | Bounds statistics = getBounds(display, messageCache.getMessage(Messages::Statistics), &MAIN_FONT); 108 | 109 | display.drawRoundRect(boxBounds.x, boxBounds.y, boxBounds.w, boxBounds.h, 10, GxEPD_BLACK); 110 | 111 | yOffset += statistics.h; 112 | drawText(display, "Statistik", boxBounds.x + innerPadding, yOffset, &MAIN_FONT, GxEPD_BLACK); 113 | 114 | yOffset += innerPadding; 115 | drawPattern(display, Pattern::SparseDots, boxBounds.x, boxBounds.y, boxBounds.w, yOffset - boxBounds.y); 116 | display.drawFastHLine(boxBounds.x, yOffset, boxBounds.w, GxEPD_BLACK); 117 | 118 | // fetch statistics 119 | unsigned int totalCycles; 120 | unsigned long totalTime, totalBreakTime; 121 | getStatistics(&totalCycles, &totalTime, &totalBreakTime); 122 | 123 | yOffset += 1 + innerPadding; 124 | 125 | Bounds textBounds; 126 | 127 | /// Current session 128 | 129 | textBounds = drawBottomAlignedText(display, messageCache.getMessage(Messages::Statistics_CurrentCycle), boxBounds.x + innerPadding, yOffset, &SUB_FONT, GxEPD_BLACK); 130 | Bounds boundsCurrentCycles = getBounds(display, String(cycles).c_str(), &SUB_FONT); 131 | drawBottomAlignedText(display, String(cycles).c_str(), boxBounds.x + boxBounds.w - innerPadding - boundsCurrentCycles.w, yOffset, &SUB_FONT, GxEPD_BLACK); 132 | 133 | yOffset += textBounds.h + innerPadding; 134 | 135 | textBounds = drawBottomAlignedText(display, messageCache.getMessage(Messages::Statistics_CurrentTime), boxBounds.x + innerPadding, yOffset, &SUB_FONT, GxEPD_BLACK); 136 | Bounds boundsCurrentTime = getBounds(display, formatDuration(minutesWorked).c_str(), &SUB_FONT); 137 | drawBottomAlignedText(display, formatDuration(minutesWorked).c_str(), boxBounds.x + boxBounds.w - innerPadding - boundsCurrentTime.w, yOffset, &SUB_FONT, GxEPD_BLACK); 138 | 139 | yOffset += textBounds.h + innerPadding; 140 | 141 | textBounds = drawBottomAlignedText(display, messageCache.getMessage(Messages::Statistics_CurrentBreakTime), boxBounds.x + innerPadding, yOffset, &SUB_FONT, GxEPD_BLACK); 142 | Bounds boundsCurrentBreakTime = getBounds(display, formatDuration(minutesOnBreak).c_str(), &SUB_FONT); 143 | drawBottomAlignedText(display, formatDuration(minutesOnBreak).c_str(), boxBounds.x + boxBounds.w - innerPadding - boundsCurrentBreakTime.w, yOffset, &SUB_FONT, GxEPD_BLACK); 144 | 145 | yOffset += textBounds.h + innerPadding; 146 | 147 | // Divider 148 | 149 | yOffset += 2 * innerPadding; 150 | 151 | textBounds = drawBottomAlignedText(display, "Gesamt", boxBounds.x + innerPadding, yOffset + innerPadding, &SUB_FONT, GxEPD_BLACK); 152 | drawPattern(display, Pattern::Dots, boxBounds.x, yOffset, boxBounds.w, textBounds.h + 2 * innerPadding); 153 | 154 | yOffset += textBounds.h + 2 * innerPadding + innerPadding; 155 | 156 | // All time stats 157 | 158 | textBounds = drawBottomAlignedText(display, messageCache.getMessage(Messages::Statistics_TotalCycles), boxBounds.x + innerPadding, yOffset, &SUB_FONT, GxEPD_BLACK); 159 | 160 | Bounds boundsTotalCycles = getBounds(display, String(totalCycles).c_str(), &SUB_FONT); 161 | drawBottomAlignedText(display, String(totalCycles).c_str(), boxBounds.x + boxBounds.w - innerPadding - boundsTotalCycles.w, yOffset, &SUB_FONT, GxEPD_BLACK); 162 | 163 | yOffset += textBounds.h + innerPadding; 164 | 165 | textBounds = drawBottomAlignedText(display, messageCache.getMessage(Messages::Statistics_TotalTime), boxBounds.x + innerPadding, yOffset, &SUB_FONT, GxEPD_BLACK); 166 | 167 | auto totalTimeStr = formatDuration(totalTime); 168 | Bounds boundsTotalTime = getBounds(display, String(totalTimeStr).c_str(), &SUB_FONT); 169 | drawBottomAlignedText(display, String(totalTimeStr).c_str(), boxBounds.x + boxBounds.w - innerPadding - boundsTotalTime.w, yOffset, &SUB_FONT, GxEPD_BLACK); 170 | 171 | yOffset += textBounds.h + innerPadding; 172 | 173 | textBounds = drawBottomAlignedText(display, messageCache.getMessage(Messages::Statistics_TotalBreakTime), boxBounds.x + innerPadding, yOffset, &SUB_FONT, GxEPD_BLACK); 174 | 175 | auto totalBreakTimeStr = formatDuration(totalBreakTime); 176 | Bounds boundsTotalBreakTime = getBounds(display, String(totalBreakTimeStr).c_str(), &SUB_FONT); 177 | drawBottomAlignedText(display, String(totalBreakTimeStr).c_str(), boxBounds.x + boxBounds.w - innerPadding - boundsTotalBreakTime.w, yOffset, &SUB_FONT, GxEPD_BLACK); 178 | 179 | yOffset += textBounds.h + innerPadding; 180 | } 181 | 182 | drawMenuBar(); 183 | 184 | unsigned int remainingUnit = 0; 185 | char buffer[64]; // Increased buffer size to be safe 186 | 187 | const unsigned int remainingMillis = currentBreakDuration - elapsed; 188 | const unsigned int seconds = remainingMillis / 1000; 189 | const unsigned int minutes = max(seconds / 60, 1u); 190 | uint16_t roundedSeconds = (seconds + 9) / 10 * 10; 191 | 192 | if (roundedSeconds >= 60) 193 | { 194 | sprintf(buffer, "%s - %d %s", messageCache.getMessage(isLongBreak ? Messages::Break_LongPauseText : Messages::Break_PauseText), minutes, messageCache.getMessage(Messages::TimeFormat_Minutes)); 195 | } 196 | else 197 | { 198 | if (redrawInterval != REDRAW_INTERVAL_FAST) 199 | { 200 | redrawInterval = REDRAW_INTERVAL_FAST; 201 | needsRedraw = true; 202 | } 203 | 204 | // Round to nearest 10 seconds when below 1 minute 205 | sprintf(buffer, "%s - %d %s", messageCache.getMessage(isLongBreak ? Messages::Break_LongPauseText : Messages::Break_PauseText), roundedSeconds, messageCache.getMessage(Messages::TimeFormat_Seconds)); 206 | } 207 | 208 | Bounds boundsMin = getBounds(display, buffer, &MAIN_FONT); 209 | 210 | // Draw text in the center 211 | drawText(display, buffer, display.width() / 2 - boundsMin.w / 2, display.height() - 64 / 2 + boundsMin.h / 2, &MAIN_FONT, GxEPD_BLACK); 212 | } -------------------------------------------------------------------------------- /src/states/timer_selecting_preset.cpp: -------------------------------------------------------------------------------- 1 | #include "../timer.h" 2 | 3 | #define THRESHOLD 5 4 | 5 | void Timer::handleSelectingPreset(volatile int *encoderCount) 6 | { 7 | if (Button::instance->checkAndClearButtonPress()) 8 | { 9 | Serial.printf("Timer::handleSelectingPreset: starting with preset %d\n", presetIndex); 10 | start(); 11 | topMenu->setEncoderCount(*encoderCount); 12 | needsFullRedraw = true; 13 | } 14 | 15 | if (*encoderCount != lastEncoderCount) 16 | { 17 | int change = *encoderCount - lastEncoderCount; 18 | Serial.printf("Timer::handleSelectingPreset: encoder delta %d\n", change); 19 | 20 | if (change < 0) 21 | { 22 | previousPreset(); 23 | } 24 | else 25 | { 26 | nextPreset(); 27 | } 28 | 29 | Serial.printf("Timer::handleSelectingPreset: selected preset %d (%s)\n", presetIndex, currentPreset->getName()); 30 | needsRedraw = true; 31 | lastEncoderCount = *encoderCount; 32 | } 33 | } 34 | 35 | void Timer::drawPresetSelection() 36 | { 37 | const unsigned int padding = 12; 38 | const unsigned int paddingBetweenBoxes = 18; 39 | const unsigned int paddingInsideBox = 10; 40 | const unsigned int totalWidth = display.width() - (padding * 2); 41 | const unsigned int boxWidth = (totalWidth + paddingBetweenBoxes) / presets.size() - paddingBetweenBoxes; 42 | 43 | unsigned int yOffset = padding; 44 | 45 | display.fillScreen(GxEPD_WHITE); 46 | 47 | for (int i = 0; i < presets.size(); i++) 48 | { 49 | auto &preset = presets[i]; 50 | const unsigned int xOffset = padding + (i * (boxWidth + paddingBetweenBoxes)); 51 | const unsigned int hCenter = xOffset + (boxWidth / 2); 52 | 53 | ScaledIcon icon = preset.getIcon()->scaled(192); 54 | const unsigned int iconSize = icon.size; 55 | 56 | #ifdef DEBUG 57 | display.drawLine(hCenter, 0, hCenter, display.height(), GxEPD_BLACK); 58 | #endif 59 | 60 | unsigned int yOffset = padding; 61 | 62 | display.drawRoundRect(xOffset, padding, boxWidth, display.height() - (padding * 2), 10, GxEPD_BLACK); 63 | 64 | if (currentPreset == &preset) 65 | { 66 | display.drawRoundRect(xOffset + 1, padding + 1, boxWidth - 2, display.height() - (padding * 2) - 2, 10, GxEPD_BLACK); 67 | drawPatternInRoundedArea(display, xOffset + 2, padding + 2, boxWidth - 4, display.height() - (padding * 2) - 4, 10, Pattern::VerySparseDots); 68 | } 69 | 70 | yOffset += paddingInsideBox; 71 | display.drawBitmap(hCenter - iconSize / 2, yOffset, icon.data, iconSize, iconSize, GxEPD_BLACK); 72 | 73 | #ifdef DEBUG 74 | display.fillRect(hCenter - iconSize / 2, yOffset, iconSize, iconSize, GxEPD_BLACK); 75 | #endif 76 | 77 | yOffset += iconSize + paddingInsideBox + 48; 78 | 79 | const Bounds bounds = getBounds(display, preset.getName(), &MAIN_FONT); 80 | drawText(display, preset.getName(), hCenter - bounds.w / 2, yOffset, &MAIN_FONT, GxEPD_BLACK); 81 | 82 | yOffset += 24 + 48; 83 | const unsigned int minutes = preset.getDuration() / 1000 / 60; 84 | 85 | // put the text on the right side of the box, on top of the progress bar 86 | { 87 | char buffer[3]; 88 | sprintf(buffer, "%d", minutes); 89 | Bounds bounds = getBounds(display, buffer, &SECONDARY_FONT); 90 | Bounds boundsMin = getBounds(display, "min", &SUB_FONT); 91 | const int16_t paddingBetweenText = 4; 92 | const int16_t endOfBoxContent = xOffset + boxWidth - paddingInsideBox; 93 | 94 | yOffset += 24; 95 | 96 | drawText(display, buffer, endOfBoxContent - bounds.w - paddingBetweenText - boundsMin.w, yOffset, &SECONDARY_FONT, GxEPD_BLACK); 97 | drawText(display, "min", endOfBoxContent - boundsMin.w, yOffset, &SUB_FONT, GxEPD_BLACK); 98 | 99 | yOffset += (max(bounds.h, boundsMin.h) / 2) + 4; 100 | 101 | // display.drawFastHLine(xOffset + paddingInsideBox, yOffset, boxWidth - paddingBetweenBoxes - paddingInsideBox * 2, GxEPD_BLACK); 102 | drawPattern(display, Pattern::Dots, xOffset + paddingInsideBox + 4, yOffset, boxWidth - paddingBetweenBoxes - paddingInsideBox * 2 - 8, 2); 103 | 104 | yOffset += 8; 105 | 106 | // draw the pause duration 107 | const unsigned int pauseMinutes = preset.getPauseDuration() / 1000 / 60; 108 | char pauseBuffer[3]; 109 | sprintf(pauseBuffer, "%d", pauseMinutes); 110 | Bounds pauseBounds = getBounds(display, pauseBuffer, &SECONDARY_FONT); 111 | Bounds pauseBoundsMin = getBounds(display, "min", &SUB_FONT); 112 | 113 | const auto breakIcon = icon_coffee.scaled(48); 114 | const unsigned int pauseIconSize = breakIcon.size; 115 | const unsigned int pauseIconX = xOffset + paddingInsideBox; 116 | 117 | display.drawBitmap(pauseIconX, yOffset, breakIcon.data, pauseIconSize, pauseIconSize, GxEPD_BLACK); 118 | 119 | drawText(display, pauseBuffer, endOfBoxContent - pauseBounds.w - paddingBetweenText - pauseBoundsMin.w, yOffset + pauseIconSize - 8, &SECONDARY_FONT, GxEPD_BLACK); 120 | drawText(display, "min", endOfBoxContent - pauseBoundsMin.w, yOffset + pauseIconSize - 8, &SUB_FONT, GxEPD_BLACK); 121 | } 122 | } 123 | } -------------------------------------------------------------------------------- /src/states/timer_waiting_for_confirmation.cpp: -------------------------------------------------------------------------------- 1 | #include "../timer.h" 2 | 3 | void Timer::handleWaitingForConfirmation(volatile int *encoderCount) 4 | { 5 | elapsed = millis() - startTime - totalPausedTime; 6 | 7 | const int encoderDelta = *encoderCount - lastEncoderCount; 8 | if (encoderDelta != 0) 9 | { 10 | if (encoderDelta > 0) 11 | { 12 | confirmationMenu->next(); 13 | } 14 | else 15 | { 16 | confirmationMenu->previous(); 17 | } 18 | needsRedraw = true; 19 | lastEncoderCount = *encoderCount; 20 | } 21 | 22 | if (Button::instance->checkAndClearButtonPress()) 23 | { 24 | if (state == TimerState::WaitingConfirmStartOfBreak) 25 | { 26 | Serial.println("Timer::handleWaitingForConfirmation: confirmed start of break"); 27 | messageCache.clearCache(Messages::TimerWaitingForConfirmationStartOfBreak_Header); 28 | startBreak(); 29 | needsFullRedraw = true; 30 | } 31 | else 32 | { 33 | Serial.println("Timer::handleWaitingForConfirmation: handling end of break"); 34 | messageCache.clearCache(Messages::TimerWaitingForConfirmationEndOfBreak_Header); 35 | 36 | incrementTotalBreakTime(elapsed); 37 | minutesOnBreak += elapsed / 1000 / 60; 38 | 39 | // Handle menu selection 40 | if (confirmationMenu->getSelectedIndex() == 0) 41 | { 42 | // Restart timer 43 | start(); 44 | } 45 | else 46 | { 47 | // Back to presets 48 | reset(); 49 | enterPresetSelection(); 50 | } 51 | needsFullRedraw = true; 52 | } 53 | } 54 | 55 | if (millis() - lastRedrawTime >= redrawInterval) 56 | { 57 | flashingIcon = !flashingIcon; 58 | needsRedraw = true; 59 | } 60 | } 61 | 62 | void Timer::drawWaitingForConfirmation() 63 | { 64 | // const auto foreground = flashingIcon ? GxEPD_BLACK : GxEPD_WHITE; 65 | // const auto background = flashingIcon ? GxEPD_WHITE : GxEPD_BLACK; 66 | const auto foreground = GxEPD_BLACK; 67 | const auto background = GxEPD_WHITE; 68 | 69 | display.fillScreen(background); 70 | 71 | ScaledIcon icon = icon_lpetantrum.scaled(128); 72 | 73 | const char *text = state == TimerState::WaitingConfirmStartOfBreak ? messageCache.getMessage(Messages::TimerWaitingForConfirmationStartOfBreak_Header) : messageCache.getMessage(Messages::TimerWaitingForConfirmationEndOfBreak_Header); 74 | Bounds textBounds = getBounds(display, text, &SEMI_LARGE_FONT); 75 | 76 | const int16_t padding = 20; 77 | const auto boxWidth = padding + icon.size + padding + textBounds.w + padding; 78 | const auto boxHeight = icon.size + padding * 2; 79 | const auto boxX = display.width() / 2 - boxWidth / 2; 80 | const auto boxY = display.height() / 2 - boxHeight / 2; 81 | 82 | // Draw main box with icon and text 83 | display.drawRoundRect(boxX, boxY, boxWidth, boxHeight, 10, foreground); 84 | 85 | // Draw icon on the left 86 | display.drawBitmap(boxX + padding, boxY + padding, icon.data, icon.size, icon.size, foreground); 87 | 88 | // Draw main text vertically centered with icon 89 | drawText(display, text, 90 | boxX + padding + icon.size + padding, 91 | boxY + boxHeight / 2 + textBounds.h / 2, 92 | &SEMI_LARGE_FONT, foreground); 93 | 94 | // Draw menu options - either single item or two items based on state 95 | const uint16_t menuY = boxY + boxHeight + padding; 96 | const uint16_t paddingBetweenBoxes = 20; 97 | const uint16_t menuItemCount = (state == TimerState::WaitingConfirmStartOfBreak) ? 1 : 2; 98 | const uint16_t menuItemWidth = (state == TimerState::WaitingConfirmStartOfBreak) ? boxWidth : (boxWidth - paddingBetweenBoxes) / 2; 99 | const uint16_t menuHeight = 48; 100 | 101 | for (int i = 0; i < menuItemCount; i++) 102 | { 103 | const bool selected = i == confirmationMenu->getSelectedIndex(); 104 | auto item = confirmationMenu->getItems()[i]; 105 | const uint16_t xOffset = (state == TimerState::WaitingConfirmStartOfBreak) ? boxX : boxX + i * (menuItemWidth + paddingBetweenBoxes); 106 | 107 | if (selected) 108 | { 109 | display.fillRoundRect(xOffset, menuY, menuItemWidth, menuHeight, 10, GxEPD_WHITE); 110 | drawPatternInRoundedArea(display, xOffset, menuY, menuItemWidth, menuHeight, 10, Pattern::SparseDots); 111 | } 112 | else 113 | { 114 | display.fillRoundRect(xOffset, menuY, menuItemWidth, menuHeight, 10, GxEPD_WHITE); 115 | } 116 | display.drawRoundRect(xOffset, menuY, menuItemWidth, menuHeight, 10, GxEPD_BLACK); 117 | drawCenteredText(display, item.getText(), xOffset + menuItemWidth / 2, menuY + menuHeight / 2, &SUB_FONT, GxEPD_BLACK); 118 | } 119 | } -------------------------------------------------------------------------------- /src/statistics.cpp: -------------------------------------------------------------------------------- 1 | #include "statistics.h" 2 | #include "preferences_manager.h" 3 | 4 | extern Preferences preferences; 5 | 6 | void incrementTotalCycles() 7 | { 8 | Serial.printf("Statistics: incrementTotalCycles (+1)\n"); 9 | unsigned int totalCycles = pref_getStatistic("tcs", (unsigned int)0); 10 | totalCycles++; 11 | pref_putStatistic("tcs", totalCycles); 12 | } 13 | 14 | void incrementTotalTime(unsigned long ms) 15 | { 16 | Serial.printf("Statistics: incrementTotalTime (%lu [+%lu])\n", ms, ms / 1000 / 60); 17 | unsigned long totalTime = pref_getStatistic("tt", (unsigned long)0); 18 | totalTime += ms / 1000 / 60; 19 | pref_putStatistic("tt", totalTime); 20 | } 21 | 22 | void incrementTotalBreakTime(unsigned long ms) 23 | { 24 | Serial.printf("Statistics: incrementTotalBreakTime (%lu [+%lu])\n", ms, ms / 1000 / 60); 25 | unsigned long totalBreakTime = pref_getStatistic("tbt", (unsigned long)0); 26 | totalBreakTime += ms / 1000 / 60; 27 | pref_putStatistic("tbt", totalBreakTime); 28 | } 29 | 30 | void getStatistics(unsigned int *totalCycles, unsigned long *totalTime, unsigned long *totalBreakTime) 31 | { 32 | *totalCycles = pref_getStatistic("tcs", (unsigned int)0); 33 | *totalTime = pref_getStatistic("tt", (unsigned long)0); 34 | *totalBreakTime = pref_getStatistic("tbt", (unsigned long)0); 35 | 36 | Serial.printf("Statistics: getStatistics: totalCycles=%d, totalTime=%lu, totalBreakTime=%lu\n", *totalCycles, *totalTime, *totalBreakTime); 37 | } 38 | 39 | void resetStatistics() 40 | { 41 | Serial.println("Statistics: resetStatistics"); 42 | pref_putStatistic("tcs", (unsigned int)0); 43 | pref_putStatistic("tt", (unsigned long)0); 44 | pref_putStatistic("tbt", (unsigned long)0); 45 | } -------------------------------------------------------------------------------- /src/statistics.h: -------------------------------------------------------------------------------- 1 | #ifndef STATISTICS_H 2 | #define STATISTICS_H 3 | 4 | #include 5 | #include 6 | 7 | void incrementTotalCycles(); 8 | void incrementTotalTime(unsigned long ms); 9 | void incrementTotalBreakTime(unsigned long ms); 10 | 11 | void getStatistics(unsigned int *totalCycles, unsigned long *totalTime, unsigned long *totalBreakTime); 12 | void resetStatistics(); 13 | 14 | #endif -------------------------------------------------------------------------------- /src/strings.cpp: -------------------------------------------------------------------------------- 1 | #include "strings.h" 2 | 3 | MessageCache messageCache; 4 | 5 | bool MessageCache::isLpeModeEnabled() 6 | { 7 | return pref_getCheckbox("lpe", false); 8 | } 9 | 10 | static const char *randomMessage(const std::vector &messages) 11 | { 12 | if (messages.empty()) 13 | return "???"; 14 | return messages[random(0, messages.size())]; 15 | } 16 | 17 | static const std::vector genericPresetMessages = { 18 | "Okaaay, let's go!", 19 | "Heute schon auf Reddit gewesen?", 20 | "Bist du hydriert?", 21 | "Vielleicht eine kleine Kaffeepause?", 22 | "Langsam ist aber auch Zeit fuer\nFeierabend, oder nicht?", 23 | "Schoen hier, aber warst du heute\nschon auf Reddit?", 24 | "Du schaffst das (hoffen wir)", 25 | "Schauen wir mal was wird\n\n was wird", 26 | }; 27 | 28 | static const std::vector chatGptFacts = { 29 | "Das Gehirn eines Elefanten\nenthaelt ueber 257 Mrd. Neuronen\nund zeigt starke Emotionen.", 30 | 31 | "Tintenfische besitzen drei Herzen\nund ein ausgekluegeltes Nervensystem,\ndas Probleme effizient loest.", 32 | 33 | "Voegel besitzen Magnetrezeptoren,\ndie ihnen helfen, das Erdmagnetfeld\nals Kompass zu nutzen.", 34 | 35 | "Bienen kommunizieren durch einen\npraezisen Schwaenzeltanz, mit dem sie\nFutterquellen uebermitteln.", 36 | 37 | "Schlangen spueren Waerme ueber\nspezialisierte Rezeptoren,\ndie sie zu geschickten Jaegern machen.", 38 | 39 | "Lungenfische atmen mit Kiemen\nund primitiven Lungen,\nueberbruecken so Wasser und Land.", 40 | 41 | "Chamaeleons aendern ihre Farbe\nnicht nur zur Tarnung,\nsondern auch zur Kommunikation.", 42 | 43 | "Kojoten sind sehr anpassungsfaehig\nund leben in verschieden Umgebungen,\nvon Wuesten bis zu Staedten.", 44 | 45 | "Kolibris sind die einzigen Voegel,\ndie rueckwaerts fliegen koennen,\ndank spezieller Flugmuskulatur.", 46 | 47 | "Giraffen haben ein komplexes\nBlutkreislaufsystem, das ihren Kopf\nmit sauerstoffreichem Blut versorgt.", 48 | 49 | "Haie besitzen Lorenzinische Ampullen,\ndie ihnen helfen, schwache\nelektrische Felder zu spueren.", 50 | 51 | "Ein Ameisenstaat kann das Gewicht\nmehrerer Elefanten tragen", 52 | 53 | "Faultiere bewegen sich sehr langsam,\ndamit Algen sich ansiedeln\nund sie gut getarnt bleiben.", 54 | 55 | "Geparden erreichen Top-\nGeschwindigkeiten, koennen\naber nur kurz laufen.", 56 | 57 | "Seesterne haben kein Gehirn,\nsondern ein verteiltes Nervensystem,\ndas in ihren Armen wirkt.", 58 | 59 | "Wale nutzen Infraschall,\nderen Toene sich ueber viele km\nim Ozean ausbreiten.", 60 | 61 | "Axolotl regenerieren Gliedmaßen,\nwas sie zu interessanten\nForschungsobjekten macht.", 62 | 63 | "Pinguine speichern Waerme,\nindem sie ihre Federn eng anlegen\nund den Waermeverlust minimieren.", 64 | 65 | "Die DNA vieler Tiere zeigt erstaunliche\nGemeinsamkeiten, die evolutionaere\nVerwandtschaften offenbaren.", 66 | 67 | "Schmetterlinge haben ausgekluegelte\nFarberkennungssysteme", 68 | 69 | "Menschen und Bananen\nteilen etwa 60% ihrer Gene,\nein Hinweis auf geteilte Wurzeln.", 70 | "Quallen haben uralte Gene,\ndie sich kaum veraendert haben.", 71 | "Koalas haben\nfingerabdruckartige Rillen,\naehnlich wie Menschen.", 72 | "Haie und Rochen\nteilen genetische Wurzeln\nund gehoeren zur Knorpelfisch-Familie.", 73 | 74 | "Every move on the board\nis a rebellion; no piece is sacred,\nand the king is just another target.", 75 | 76 | "In chess, chaos is art,\neach pawn a revolutionary spark,\nevery check a call to arms.", 77 | 78 | "Google en passant\n\n holy hell!", 79 | 80 | "Als die Dinosaurier existierten,\ngab es Vulkane, die auf dem Mond\nausbrachen.", 81 | 82 | "Die einzigen Buchstaben, die\nnicht im Periodensystem vorkommen,\nsind 'J' und 'Q'.", 83 | 84 | "Wenn ein Eisbaer und ein\nGrizzlybaer sich paaren,\nwird 'Pizzy Bear' genannt.", 85 | 86 | "Daniel Radcliffe war allergisch gegen\nseine Harry-Potter-Brille,\ndoch Harry Potter traegt sie.", 87 | 88 | "Im Englischen heißt es 'French Exit',\nwenn man ohne Abschied geht", 89 | 90 | "In Arizona kann das Faellen eines\nSaguaro-Kaktus als Verbrechen\ngeahndet werden.", 91 | 92 | "Der in Statuen gezeigte Buddha\nist nicht der wahre Buddha;\nder echte war mager durch Askese.", 93 | 94 | "Ein einzelner Spaghetti-Strang\nwird als 'Spaghetto' bezeichnet,\neine kuriose Tatsache.", 95 | 96 | "Princess Peach blieb still\nbis 1988, da Designer\nsie nicht beweglich machten.", 97 | 98 | "Der erste Film mit Soundtrack\nwar Schneewittchen\nund die sieben Zwerge.", 99 | 100 | "Reichst du mit deinen Autoschluesseln\nan deinen Kopf, erhoeht sich\ndie Reichweite der Fernbedienung.", 101 | 102 | "Fruchtaufkleber sind essbar,\nwie auch das Obst selbst;\nVor dem Verzehr waschen!", 103 | 104 | "Der Name des Riesenameisbers\nist Myrmecophaga Tridactyla,\nwas 'Ameisenessend mit 3 Fingern' heißt.", 105 | 106 | "Das Wort 'Astronaut' kommt\naus dem Griechischen 'astro' = Stern,\nund 'naut' heißt Seefahrer." 107 | 108 | }; 109 | 110 | static const std::vector 111 | genericStartBreakMessages = {"Break time!", "Take a rest", "Time to relax", "Well done!"}; 112 | 113 | extern const std::vector lpeStartBreakMessages = {}; 114 | static const std::vector lpeEmailPresetMessages = {}; 115 | static const std::vector lpeCodingPresetMessages = {}; 116 | 117 | static const char *generateMessage(Messages message) 118 | { 119 | const char *result = ""; 120 | std::vector messages; 121 | messages.insert(messages.end(), chatGptFacts.begin(), chatGptFacts.end()); 122 | messages.insert(messages.end(), genericPresetMessages.begin(), genericPresetMessages.end()); 123 | 124 | bool lpeModeEnabled = messageCache.isLpeModeEnabled(); 125 | 126 | switch (message) 127 | { 128 | case Messages::TimerWaitingForConfirmationStartOfBreak_Header: 129 | result = "Geschafft!"; 130 | break; 131 | 132 | case Messages::TimerWaitingForConfirmationEndOfBreak_Header: 133 | result = "Pause vorbei"; 134 | break; 135 | 136 | case Messages::Break_PauseText: 137 | result = "Pause"; 138 | break; 139 | 140 | case Messages::Break_LongPauseText: 141 | result = "Lange Pause"; 142 | break; 143 | 144 | // Menu items 145 | case Messages::MenuItem_Pause: 146 | if (lpeModeEnabled) 147 | { 148 | result = "Kurz weg"; 149 | } 150 | else 151 | { 152 | result = "Pause"; 153 | } 154 | break; 155 | case Messages::MenuItem_Resume: 156 | if (lpeModeEnabled) 157 | { 158 | result = "Wieder da"; 159 | } 160 | else 161 | { 162 | result = "Resume"; 163 | } 164 | break; 165 | case Messages::MenuItem_BreakNow: 166 | if (lpeModeEnabled) 167 | { 168 | result = "GENUG!"; 169 | } 170 | else 171 | { 172 | result = "Break now"; 173 | } 174 | break; 175 | case Messages::MenuItem_SkipBreak: 176 | if (lpeModeEnabled) 177 | { 178 | result = "Skip break"; 179 | } 180 | else 181 | { 182 | result = "Skip break"; 183 | } 184 | break; 185 | case Messages::MenuItem_Cancel: 186 | result = "Stop"; 187 | break; 188 | case Messages::MenuItem_BackToPresets: 189 | result = "Zur Auswahl"; 190 | break; 191 | case Messages::MenuItem_RestartTimer: 192 | if (lpeModeEnabled) 193 | { 194 | result = randomMessage({"Noch mal!", "AGAIN!", "Here we go again...", "Do it agane"}); 195 | } 196 | else 197 | { 198 | result = randomMessage({"Restart", "Let's go again", "One more time"}); 199 | } 200 | break; 201 | case Messages::MenuItem_StartBreak: 202 | if (lpeModeEnabled) 203 | { 204 | result = randomMessage(lpeStartBreakMessages); 205 | } 206 | else 207 | { 208 | result = randomMessage(genericStartBreakMessages); 209 | } 210 | break; 211 | 212 | // Timer states 213 | case Messages::TimerState_Paused: 214 | result = "- PAUSED -"; 215 | break; 216 | 217 | // Time formats 218 | case Messages::TimeFormat_Minutes: 219 | result = "min"; 220 | break; 221 | case Messages::TimeFormat_Seconds: 222 | result = "sec"; 223 | break; 224 | 225 | // Preset specific 226 | case Messages::Preset_Email_Message: 227 | if (lpeModeEnabled) 228 | { 229 | messages.insert(messages.end(), lpeEmailPresetMessages.begin(), lpeEmailPresetMessages.end()); 230 | } 231 | result = randomMessage(messages); 232 | break; 233 | 234 | case Messages::Preset_Coding_Message: 235 | if (lpeModeEnabled) 236 | { 237 | messages.insert(messages.end(), lpeCodingPresetMessages.begin(), lpeCodingPresetMessages.end()); 238 | } 239 | result = randomMessage(messages); 240 | break; 241 | 242 | case Messages::Preset_Focus_Message: 243 | result = randomMessage(messages); 244 | break; 245 | 246 | case Messages::Statistics: 247 | result = "Statistiken"; 248 | break; 249 | 250 | case Messages::Statistics_CurrentCycle: 251 | result = "Aktueller Zyklus"; 252 | break; 253 | 254 | case Messages::Statistics_CurrentTime: 255 | result = "Arbeitszeit"; 256 | break; 257 | 258 | case Messages::Statistics_CurrentBreakTime: 259 | result = "Pausenzeit"; 260 | break; 261 | 262 | case Messages::Statistics_TotalCycles: 263 | result = "Zyklen"; 264 | break; 265 | 266 | case Messages::Statistics_TotalTime: 267 | result = "Arbeitszeit"; 268 | break; 269 | 270 | case Messages::Statistics_TotalBreakTime: 271 | result = "Pausenzeit"; 272 | break; 273 | 274 | default: 275 | result = "???"; 276 | break; 277 | } 278 | 279 | return result; 280 | } 281 | 282 | const char *MessageCache::getMessage(Messages message) 283 | { 284 | auto it = cache.find(message); 285 | if (it != cache.end()) 286 | { 287 | return it->second; 288 | } 289 | 290 | const char *result = generateMessage(message); 291 | cache[message] = result; 292 | return result; 293 | } 294 | 295 | void MessageCache::clearCache(Messages message) 296 | { 297 | cache.erase(message); 298 | } 299 | 300 | void MessageCache::clearAllCache() 301 | { 302 | cache.clear(); 303 | } 304 | 305 | std::vector MessageCache::getMessages() 306 | { 307 | std::vector messages; 308 | // append vectors 309 | messages.insert(messages.end(), genericPresetMessages.begin(), genericPresetMessages.end()); 310 | messages.insert(messages.end(), genericStartBreakMessages.begin(), genericStartBreakMessages.end()); 311 | messages.insert(messages.end(), lpeStartBreakMessages.begin(), lpeStartBreakMessages.end()); 312 | messages.insert(messages.end(), lpeEmailPresetMessages.begin(), lpeEmailPresetMessages.end()); 313 | messages.insert(messages.end(), lpeCodingPresetMessages.begin(), lpeCodingPresetMessages.end()); 314 | messages.insert(messages.end(), chatGptFacts.begin(), chatGptFacts.end()); 315 | 316 | return messages; 317 | } 318 | -------------------------------------------------------------------------------- /src/strings.h: -------------------------------------------------------------------------------- 1 | #ifndef STRINGS_H 2 | #define STRINGS_H 3 | 4 | #include 5 | #include // for float_t, double_t 6 | #include // for String 7 | #include 8 | #include "preferences_manager.h" 9 | #include 10 | #include 11 | 12 | enum class Messages 13 | { 14 | TimerWaitingForConfirmationStartOfBreak_Header, 15 | TimerWaitingForConfirmationEndOfBreak_Header, 16 | 17 | Break_PauseText, 18 | Break_LongPauseText, 19 | 20 | // Menu items 21 | MenuItem_Pause, 22 | MenuItem_Resume, 23 | MenuItem_BreakNow, 24 | MenuItem_SkipBreak, 25 | MenuItem_Cancel, 26 | MenuItem_BackToPresets, 27 | MenuItem_RestartTimer, 28 | MenuItem_StartBreak, 29 | 30 | // Timer states 31 | TimerState_Paused, 32 | 33 | // Time formats 34 | TimeFormat_Minutes, 35 | TimeFormat_Seconds, 36 | 37 | // Preset specific messages 38 | Preset_Email_Message, 39 | Preset_Coding_Message, 40 | Preset_Focus_Message, 41 | 42 | Statistics, 43 | Statistics_CurrentCycle, 44 | Statistics_CurrentTime, 45 | Statistics_CurrentBreakTime, 46 | Statistics_TotalCycles, 47 | Statistics_TotalTime, 48 | Statistics_TotalBreakTime, 49 | }; 50 | 51 | class MessageCache 52 | { 53 | private: 54 | std::map cache; 55 | 56 | public: 57 | MessageCache() 58 | { 59 | } 60 | 61 | bool isLpeModeEnabled(); 62 | 63 | const char *getMessage(Messages message); 64 | void clearCache(Messages message); 65 | void clearAllCache(); 66 | 67 | std::vector getMessages(); 68 | }; 69 | 70 | extern MessageCache messageCache; 71 | 72 | #endif -------------------------------------------------------------------------------- /src/timer.cpp: -------------------------------------------------------------------------------- 1 | #include "timer.h" 2 | #include "strings.h" 3 | #include "images.h" 4 | #include "led.h" 5 | #include "preferences_manager.h" 6 | 7 | extern Preferences preferences; 8 | 9 | Preset::Preset(Icon *icon, const unsigned char *background, const char *name, unsigned long duration, unsigned long pauseDuration, unsigned long longPauseDuration, unsigned int longPauseAfter) 10 | { 11 | this->icon = icon; 12 | this->background = background; 13 | this->name = name; 14 | this->duration = duration; 15 | this->pauseDuration = pauseDuration; 16 | this->longPauseAfter = longPauseAfter; 17 | this->longPauseDuration = longPauseDuration; 18 | } 19 | 20 | Preset::~Preset() 21 | { 22 | } 23 | 24 | Icon *Preset::getIcon() 25 | { 26 | return icon; 27 | } 28 | 29 | const unsigned char *Preset::getBackground() 30 | { 31 | return background; 32 | } 33 | 34 | unsigned long Preset::getDuration() 35 | { 36 | #ifdef DEBUG 37 | return duration / 60; 38 | #endif 39 | return duration; 40 | } 41 | 42 | unsigned long Preset::getPauseDuration() 43 | { 44 | #ifdef DEBUG 45 | return pauseDuration / 60; 46 | #endif 47 | return pauseDuration; 48 | } 49 | 50 | const char *Preset::getName() 51 | { 52 | return name; 53 | } 54 | 55 | unsigned long Preset::getLongPauseDuration() 56 | { 57 | #ifdef DEBUG 58 | return longPauseDuration / 60; 59 | #endif 60 | return longPauseDuration; 61 | } 62 | 63 | unsigned int Preset::getLongPauseAfter() 64 | { 65 | return longPauseAfter; 66 | } 67 | 68 | Timer::Timer(DISPLAY_CLASS &display) : display(display) 69 | { 70 | this->state = TimerState::SelectingPreset; 71 | this->currentPreset = nullptr; 72 | this->presetIndex = 0; 73 | this->needsRedraw = true; 74 | this->lastEncoderCount = 0; 75 | this->lastRedrawTime = 0; 76 | this->pauseStartTime = 0; 77 | this->totalPausedTime = 0; 78 | 79 | MenuItem *items = new MenuItem[3]{ 80 | MenuItem(messageCache.getMessage(Messages::MenuItem_Pause)), 81 | MenuItem(messageCache.getMessage(Messages::MenuItem_BreakNow)), 82 | MenuItem(messageCache.getMessage(Messages::MenuItem_Cancel))}; 83 | this->topMenu = new Menu(display, items, 3); 84 | this->topMenu->setSelectedIndex(1); 85 | lastEncoderCount = 0; 86 | 87 | MenuItem *confirmationItems = new MenuItem[2]{ 88 | MenuItem(messageCache.getMessage(Messages::MenuItem_StartBreak)), 89 | MenuItem(messageCache.getMessage(Messages::MenuItem_BackToPresets))}; 90 | this->confirmationMenu = new Menu(display, confirmationItems, 2); 91 | this->confirmationMenu->setEncoderCount(0); // Initialize encoder count 92 | } 93 | 94 | Timer::~Timer() 95 | { 96 | if (topMenu) 97 | { 98 | delete[] topMenu->getItems(); 99 | delete topMenu; 100 | } 101 | } 102 | 103 | void Timer::addPreset(Icon *icon, const unsigned char *background, const char *name, unsigned long duration, unsigned long pauseDuration, unsigned long longPauseDuration, unsigned int longPauseAfter) 104 | { 105 | presets.push_back(Preset(icon, background, name, duration, pauseDuration, longPauseDuration, longPauseAfter)); 106 | } 107 | 108 | void Timer::selectPreset(int index) 109 | { 110 | currentPreset = &presets[index]; 111 | presetIndex = index; 112 | } 113 | 114 | void Timer::nextPreset() 115 | { 116 | presetIndex++; 117 | if (presetIndex >= presets.size()) 118 | { 119 | presetIndex = 0; 120 | } 121 | 122 | currentPreset = &presets[presetIndex]; 123 | } 124 | 125 | void Timer::previousPreset() 126 | { 127 | if (presetIndex <= 0) 128 | { 129 | presetIndex = presets.size() - 1; 130 | } 131 | else 132 | { 133 | presetIndex--; 134 | } 135 | 136 | currentPreset = &presets[presetIndex]; 137 | } 138 | 139 | void Timer::enterPresetSelection() 140 | { 141 | state = TimerState::SelectingPreset; 142 | selectPreset(1); 143 | needsRedraw = true; 144 | } 145 | 146 | void Timer::reset() 147 | { 148 | setLedMode(LedMode::Off); 149 | redrawInterval = REDRAW_INTERVAL_DEFAULT; 150 | startTime = millis(); 151 | elapsed = 0; 152 | pauseStartTime = 0; 153 | totalPausedTime = 0; 154 | Button::instance->checkAndClearButtonPress(); 155 | topMenu->getItems()[0].setText(messageCache.getMessage(Messages::MenuItem_Pause)); 156 | topMenu->getItems()[1].setText(messageCache.getMessage(Messages::MenuItem_BreakNow)); 157 | topMenu->setSelectedIndex(1); 158 | } 159 | 160 | void Timer::start() 161 | { 162 | if (currentPreset != nullptr) 163 | { 164 | 165 | showSpeechBubble = pref_getCheckbox("msgs", true); 166 | 167 | // remember selected preset 168 | auto presetIndex = this->presetIndex; 169 | 170 | reset(); 171 | topMenu->setEncoderCount(lastEncoderCount); // Sync encoder count 172 | 173 | Serial.printf("Timer::start with preset %d\n", presetIndex); 174 | state = TimerState::Running; 175 | selectPreset(presetIndex); 176 | } 177 | } 178 | 179 | void Timer::pause() 180 | { 181 | if (state == TimerState::Running) 182 | { 183 | state = TimerState::UserInitiatedPause; 184 | pauseStartTime = millis(); 185 | setLedMode(LedMode::TimerPaused); 186 | topMenu->getItems()[0].setText(messageCache.getMessage(Messages::MenuItem_Resume)); 187 | topMenu->setEncoderCount(lastEncoderCount); // Sync encoder count 188 | } 189 | else if (state == TimerState::RunningBreak) 190 | { 191 | state = TimerState::UserInitiatedBreakPause; 192 | pauseStartTime = millis(); 193 | setLedMode(LedMode::TimerPaused); 194 | topMenu->getItems()[0].setText(messageCache.getMessage(Messages::MenuItem_Resume)); 195 | topMenu->setEncoderCount(lastEncoderCount); // Sync encoder count 196 | } 197 | } 198 | 199 | void Timer::resume() 200 | { 201 | if (state == TimerState::UserInitiatedPause) 202 | { 203 | totalPausedTime += millis() - pauseStartTime; 204 | state = TimerState::Running; 205 | setLedMode(LedMode::Off); 206 | topMenu->getItems()[0].setText(messageCache.getMessage(Messages::MenuItem_Pause)); 207 | topMenu->setEncoderCount(lastEncoderCount); // Sync encoder count 208 | } 209 | else if (state == TimerState::UserInitiatedBreakPause) 210 | { 211 | totalPausedTime += millis() - pauseStartTime; 212 | state = TimerState::RunningBreak; 213 | setLedMode(LedMode::Off); 214 | topMenu->getItems()[0].setText(messageCache.getMessage(Messages::MenuItem_Pause)); 215 | topMenu->setEncoderCount(lastEncoderCount); // Sync encoder count 216 | } 217 | } 218 | 219 | void Timer::startBreak() 220 | { 221 | cycles += 1; 222 | incrementTotalCycles(); 223 | minutesWorked += elapsed / 1000 / 60; 224 | incrementTotalTime(elapsed); 225 | 226 | Serial.printf("Timer::startBreak: cycles %d with long break after %d\n", cycles, currentPreset->getLongPauseAfter()); 227 | if (cycles % (currentPreset->getLongPauseAfter()) == 0) 228 | { 229 | Serial.println("Timer::startBreak: long break"); 230 | currentBreakDuration = currentPreset->getLongPauseDuration(); 231 | isLongBreak = true; 232 | longestEarnedPauseInShortCycles = 0; 233 | 234 | const std::vector breakImages = { 235 | image_bg_cat, 236 | image_bg_stonks, 237 | image_bg_pablo, 238 | image_bg_what_a_week, 239 | }; 240 | 241 | breakImage = breakImages.at(random(0, breakImages.size())); 242 | 243 | // HACK: The display doesn't like drawing images in one go 244 | drawRunningBreak(); 245 | display.display(true); 246 | } 247 | else 248 | { 249 | Serial.println("Timer::startBreak: short break"); 250 | currentBreakDuration = currentPreset->getPauseDuration(); 251 | isLongBreak = false; 252 | needsRedraw = true; 253 | } 254 | 255 | state = TimerState::RunningBreak; 256 | reset(); 257 | topMenu->getItems()[1].setText(messageCache.getMessage(Messages::MenuItem_SkipBreak)); 258 | 259 | // Restore the last selected index 260 | topMenu->setSelectedIndex(1); 261 | 262 | needsRedraw = true; 263 | } 264 | 265 | void Timer::stop() 266 | { 267 | setLedMode(LedMode::Off); 268 | reset(); 269 | 270 | enterPresetSelection(); 271 | } 272 | 273 | int Timer::drawMenuBar() 274 | { 275 | const unsigned int padding = 8; 276 | const unsigned int innerPadding = 4; 277 | const unsigned int iconSize = 48; 278 | 279 | Bounds bounds = getBounds(display, currentPreset->getName(), &SUB_FONT); 280 | 281 | #ifdef DEBUG 282 | display.drawRect(padding, padding, iconSize, iconSize, GxEPD_BLACK); 283 | #endif 284 | 285 | // Draw the menu next to the category 286 | // const uint16_t menuX = padding + innerPadding + iconSize + innerPadding + bounds.w + innerPadding + 8; 287 | const uint16_t menuX = padding; 288 | const uint16_t menuY = padding; 289 | const uint16_t paddingBetweenBoxes = 10; 290 | const uint16_t totalPaddingWidth = paddingBetweenBoxes * (topMenu->getItemCount() - 1); 291 | const uint16_t rawMenuWidth = display.width() - menuX - padding; 292 | const uint16_t menuItemWidth = (rawMenuWidth - (topMenu->getItemCount() - 1) * paddingBetweenBoxes) / topMenu->getItemCount(); 293 | const uint16_t menuHeight = innerPadding + 48; // iconSize; 294 | 295 | // Draw the menu 296 | for (int i = 0; i < topMenu->getItemCount(); i++) 297 | { 298 | const bool selected = i == topMenu->getSelectedIndex(); 299 | const auto foreground = GxEPD_BLACK; 300 | const auto background = GxEPD_WHITE; 301 | 302 | const unsigned int xOffset = menuX + i * (menuItemWidth + paddingBetweenBoxes); 303 | 304 | auto item = topMenu->getItems()[i]; 305 | 306 | if (selected) 307 | { 308 | display.fillRoundRect(xOffset, menuY, menuItemWidth, menuHeight, 10, GxEPD_WHITE); 309 | drawPatternInRoundedArea(display, xOffset, menuY, menuItemWidth, menuHeight, 10, Pattern::SparseDots); 310 | } 311 | else 312 | { 313 | display.fillRoundRect(xOffset, menuY, menuItemWidth, menuHeight, 10, GxEPD_WHITE); 314 | } 315 | display.drawRoundRect(xOffset, menuY, menuItemWidth, menuHeight, 10, GxEPD_BLACK); 316 | 317 | drawCenteredText(display, item.getText(), xOffset + menuItemWidth / 2, menuY + menuHeight / 2, &SUB_FONT, foreground); 318 | 319 | #ifdef DEBUG 320 | display.drawFastVLine(xOffset + menuItemWidth / 2, menuY, menuHeight, GxEPD_BLACK); 321 | display.drawFastHLine(xOffset, menuY + menuHeight / 2, menuItemWidth, GxEPD_BLACK); 322 | display.drawCircle(xOffset + menuItemWidth / 2, menuY + menuHeight / 2, 5, GxEPD_BLACK); 323 | #endif 324 | } 325 | 326 | return menuY + menuHeight; 327 | } 328 | 329 | void Timer::loop(volatile int *encoderCount) 330 | { 331 | auto start = millis(); 332 | 333 | switch (state) 334 | { 335 | case TimerState::SelectingPreset: 336 | handleSelectingPreset(encoderCount); 337 | break; 338 | case TimerState::UserInitiatedPause: 339 | case TimerState::Running: 340 | handleRunning(encoderCount); 341 | break; 342 | case TimerState::UserInitiatedBreakPause: 343 | case TimerState::RunningBreak: 344 | handleRunningBreak(encoderCount); 345 | break; 346 | case TimerState::WaitingConfirmEndOfBreak: 347 | case TimerState::WaitingConfirmStartOfBreak: 348 | handleWaitingForConfirmation(encoderCount); 349 | break; 350 | default: 351 | Serial.printf("Timer::loop: unknown state %d\n", state); 352 | break; 353 | } 354 | 355 | if (menuNeedsRedraw) 356 | { 357 | auto millisMenuStart = millis(); 358 | drawMenuBar(); 359 | display.displayWindow(0, 0, display.width(), 8 + 4 + 48 + 4); 360 | Serial.printf("Timer::loop: menu update took %d ms\n", millis() - millisMenuStart); 361 | 362 | menuNeedsRedraw = false; 363 | 364 | if (!needsRedraw && !needsFullRedraw) 365 | { 366 | Serial.printf("Timer::loop: display update took %d ms\n", millis() - start); 367 | } 368 | } 369 | 370 | if (needsRedraw || needsFullRedraw) 371 | { 372 | Serial.printf("Timer::loop: needs redraw with state %d\n", state); 373 | 374 | display.firstPage(); 375 | 376 | if (needsFullRedraw) 377 | { 378 | // display.clearScreen(); 379 | display.fillScreen(GxEPD_WHITE); 380 | } 381 | 382 | switch (state) 383 | { 384 | case TimerState::SelectingPreset: 385 | Serial.println("Timer::loop: drawPresetSelection"); 386 | drawPresetSelection(); 387 | break; 388 | case TimerState::UserInitiatedPause: 389 | case TimerState::Running: 390 | Serial.println("Timer::loop: drawRunning"); 391 | drawRunning(); 392 | break; 393 | case TimerState::UserInitiatedBreakPause: 394 | case TimerState::RunningBreak: 395 | Serial.println("Timer::loop: drawRunningBreak"); 396 | drawRunningBreak(); 397 | break; 398 | case TimerState::WaitingConfirmEndOfBreak: 399 | case TimerState::WaitingConfirmStartOfBreak: 400 | Serial.println("Timer::loop: drawWaitingForConfirmation"); 401 | drawWaitingForConfirmation(); 402 | break; 403 | default: 404 | Serial.printf("Timer::loop: unknown state %d\n", state); 405 | break; 406 | } 407 | 408 | if (needsFullRedraw) 409 | { 410 | Serial.println("Timer::loop: full display update"); 411 | display.display(false); 412 | } 413 | else 414 | { 415 | Serial.println("Timer::loop: partial display update"); 416 | display.display(true); 417 | } 418 | 419 | menuNeedsRedraw = false; 420 | needsRedraw = false; 421 | needsFullRedraw = false; 422 | lastRedrawTime = millis(); 423 | Serial.printf("Timer::loop: display update took %d ms\n", millis() - start); 424 | } 425 | } 426 | 427 | TimerState Timer::getState() 428 | { 429 | return state; 430 | } -------------------------------------------------------------------------------- /src/timer.h: -------------------------------------------------------------------------------- 1 | #ifndef TIMER_H 2 | #define TIMER_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "defs.h" 10 | 11 | #define ENCODER_SW 14 // Optional push button pin 12 | 13 | #include "gfx_utils.h" 14 | #include "icons.h" 15 | #include "debug.h" 16 | #include "menu.h" 17 | #include "led.h" 18 | #include "button.h" 19 | #include "statistics.h" 20 | 21 | class Preset 22 | { 23 | private: 24 | Icon *icon; 25 | const unsigned char *background; 26 | const char *name; 27 | unsigned long duration; 28 | unsigned long pauseDuration; 29 | unsigned int longPauseAfter; 30 | unsigned long longPauseDuration; 31 | 32 | public: 33 | Preset(Icon *icon, const unsigned char *background, const char *name, unsigned long duration, unsigned long pauseDuration, unsigned long longPauseDuration, unsigned int longPauseAfter); 34 | ~Preset(); 35 | Icon *getIcon(); 36 | const unsigned char *getBackground(); 37 | unsigned long getDuration(); 38 | unsigned long getPauseDuration(); 39 | unsigned long getLongPauseDuration(); 40 | unsigned int getLongPauseAfter(); 41 | 42 | const char *getName(); 43 | }; 44 | 45 | enum class TimerState 46 | { 47 | SelectingPreset, 48 | Running, 49 | WaitingConfirmStartOfBreak, 50 | RunningBreak, 51 | WaitingConfirmEndOfBreak, 52 | UserInitiatedPause, 53 | UserInitiatedBreakPause, 54 | Stopped 55 | }; 56 | 57 | class Timer 58 | { 59 | private: 60 | DISPLAY_CLASS &display; 61 | std::vector presets; 62 | Preset *currentPreset; 63 | unsigned int presetIndex; 64 | TimerState state; 65 | unsigned long startTime; 66 | unsigned long elapsed; 67 | unsigned long pauseStartTime; 68 | unsigned long totalPausedTime; 69 | 70 | bool isLongBreak = false; 71 | unsigned int cycles = 0; 72 | unsigned long minutesWorked = 0; 73 | unsigned long minutesOnBreak = 0; 74 | unsigned long longestEarnedPauseInShortCycles = 0; 75 | unsigned long currentBreakDuration = 0; 76 | 77 | Menu *topMenu; 78 | bool menuNeedsRedraw; 79 | 80 | Menu *confirmationMenu; 81 | bool flashingIcon; 82 | 83 | const unsigned char *breakImage; 84 | 85 | bool needsRedraw; 86 | bool needsFullRedraw; 87 | int lastEncoderCount; 88 | unsigned long lastRedrawTime; 89 | unsigned long lastPauseRedrawTime; 90 | bool lastPauseState; 91 | static const unsigned long REDRAW_INTERVAL_DEFAULT = 5000; // ms 92 | static const unsigned long REDRAW_INTERVAL_FAST = 1000; // ms 93 | static const unsigned long RUNNING_MESSAGE_REFRESH_INTERVAL = 5 * 60 * 1000; // ms 94 | unsigned long redrawInterval = REDRAW_INTERVAL_DEFAULT; 95 | 96 | bool showSpeechBubble = true; 97 | 98 | void drawWaitingForConfirmation(); 99 | void drawPresetSelection(); 100 | void drawPartialPresetSelection(); 101 | int drawMenuBar(); 102 | void drawRunning(); 103 | void handleWaitingForConfirmation(volatile int *encoderCount); 104 | void handleSelectingPreset(volatile int *encoderCount); 105 | void handleRunning(volatile int *encoderCount); 106 | void handleRunningBreak(volatile int *encoderCount); 107 | void drawRunningBreak(); 108 | 109 | const char *getRunningMessage(); 110 | unsigned long lastMessageUpdate; 111 | 112 | void reset(); 113 | 114 | public: 115 | Timer(DISPLAY_CLASS &display); 116 | ~Timer(); 117 | void addPreset(Icon *icon, const unsigned char *background, const char *name, unsigned long duration, unsigned long pauseDuration, unsigned long longPauseDuration, unsigned int longPauseAfter = 4); 118 | void selectPreset(int index); 119 | void nextPreset(); 120 | void previousPreset(); 121 | void enterPresetSelection(); 122 | void start(); 123 | void pause(); 124 | void resume(); 125 | void startBreak(); 126 | void stop(); 127 | void loop(volatile int *encoderCount); 128 | TimerState getState(); 129 | bool checkAndClearButtonPress(); 130 | }; 131 | 132 | #endif 133 | -------------------------------------------------------------------------------- /stl/Pomodoro - Cover.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rukenshia/pomodoro/2c339876d54325ed6505e066731f024be315d8aa/stl/Pomodoro - Cover.stl -------------------------------------------------------------------------------- /stl/Pomodoro - Housing.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rukenshia/pomodoro/2c339876d54325ed6505e066731f024be315d8aa/stl/Pomodoro - Housing.stl -------------------------------------------------------------------------------- /stl/Pomodoro - Knob.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rukenshia/pomodoro/2c339876d54325ed6505e066731f024be315d8aa/stl/Pomodoro - Knob.stl -------------------------------------------------------------------------------- /stl/Pomodoro - LEDCase.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rukenshia/pomodoro/2c339876d54325ed6505e066731f024be315d8aa/stl/Pomodoro - LEDCase.stl -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rukenshia/pomodoro/2c339876d54325ed6505e066731f024be315d8aa/test/README.md -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 1 3 | requires-python = ">=3.13" 4 | 5 | [[package]] 6 | name = "pillow" 7 | version = "11.1.0" 8 | source = { registry = "https://pypi.org/simple" } 9 | sdist = { url = "https://files.pythonhosted.org/packages/f3/af/c097e544e7bd278333db77933e535098c259609c4eb3b85381109602fb5b/pillow-11.1.0.tar.gz", hash = "sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20", size = 46742715 } 10 | wheels = [ 11 | { url = "https://files.pythonhosted.org/packages/b3/31/9ca79cafdce364fd5c980cd3416c20ce1bebd235b470d262f9d24d810184/pillow-11.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae98e14432d458fc3de11a77ccb3ae65ddce70f730e7c76140653048c71bfcbc", size = 3226640 }, 12 | { url = "https://files.pythonhosted.org/packages/ac/0f/ff07ad45a1f172a497aa393b13a9d81a32e1477ef0e869d030e3c1532521/pillow-11.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cc1331b6d5a6e144aeb5e626f4375f5b7ae9934ba620c0ac6b3e43d5e683a0f0", size = 3101437 }, 13 | { url = "https://files.pythonhosted.org/packages/08/2f/9906fca87a68d29ec4530be1f893149e0cb64a86d1f9f70a7cfcdfe8ae44/pillow-11.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:758e9d4ef15d3560214cddbc97b8ef3ef86ce04d62ddac17ad39ba87e89bd3b1", size = 4326605 }, 14 | { url = "https://files.pythonhosted.org/packages/b0/0f/f3547ee15b145bc5c8b336401b2d4c9d9da67da9dcb572d7c0d4103d2c69/pillow-11.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b523466b1a31d0dcef7c5be1f20b942919b62fd6e9a9be199d035509cbefc0ec", size = 4411173 }, 15 | { url = "https://files.pythonhosted.org/packages/b1/df/bf8176aa5db515c5de584c5e00df9bab0713548fd780c82a86cba2c2fedb/pillow-11.1.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:9044b5e4f7083f209c4e35aa5dd54b1dd5b112b108648f5c902ad586d4f945c5", size = 4369145 }, 16 | { url = "https://files.pythonhosted.org/packages/de/7c/7433122d1cfadc740f577cb55526fdc39129a648ac65ce64db2eb7209277/pillow-11.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:3764d53e09cdedd91bee65c2527815d315c6b90d7b8b79759cc48d7bf5d4f114", size = 4496340 }, 17 | { url = "https://files.pythonhosted.org/packages/25/46/dd94b93ca6bd555588835f2504bd90c00d5438fe131cf01cfa0c5131a19d/pillow-11.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31eba6bbdd27dde97b0174ddf0297d7a9c3a507a8a1480e1e60ef914fe23d352", size = 4296906 }, 18 | { url = "https://files.pythonhosted.org/packages/a8/28/2f9d32014dfc7753e586db9add35b8a41b7a3b46540e965cb6d6bc607bd2/pillow-11.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b5d658fbd9f0d6eea113aea286b21d3cd4d3fd978157cbf2447a6035916506d3", size = 4431759 }, 19 | { url = "https://files.pythonhosted.org/packages/33/48/19c2cbe7403870fbe8b7737d19eb013f46299cdfe4501573367f6396c775/pillow-11.1.0-cp313-cp313-win32.whl", hash = "sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9", size = 2291657 }, 20 | { url = "https://files.pythonhosted.org/packages/3b/ad/285c556747d34c399f332ba7c1a595ba245796ef3e22eae190f5364bb62b/pillow-11.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:593c5fd6be85da83656b93ffcccc2312d2d149d251e98588b14fbc288fd8909c", size = 2626304 }, 21 | { url = "https://files.pythonhosted.org/packages/e5/7b/ef35a71163bf36db06e9c8729608f78dedf032fc8313d19bd4be5c2588f3/pillow-11.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:11633d58b6ee5733bde153a8dafd25e505ea3d32e261accd388827ee987baf65", size = 2375117 }, 22 | { url = "https://files.pythonhosted.org/packages/79/30/77f54228401e84d6791354888549b45824ab0ffde659bafa67956303a09f/pillow-11.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:70ca5ef3b3b1c4a0812b5c63c57c23b63e53bc38e758b37a951e5bc466449861", size = 3230060 }, 23 | { url = "https://files.pythonhosted.org/packages/ce/b1/56723b74b07dd64c1010fee011951ea9c35a43d8020acd03111f14298225/pillow-11.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8000376f139d4d38d6851eb149b321a52bb8893a88dae8ee7d95840431977081", size = 3106192 }, 24 | { url = "https://files.pythonhosted.org/packages/e1/cd/7bf7180e08f80a4dcc6b4c3a0aa9e0b0ae57168562726a05dc8aa8fa66b0/pillow-11.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ee85f0696a17dd28fbcfceb59f9510aa71934b483d1f5601d1030c3c8304f3c", size = 4446805 }, 25 | { url = "https://files.pythonhosted.org/packages/97/42/87c856ea30c8ed97e8efbe672b58c8304dee0573f8c7cab62ae9e31db6ae/pillow-11.1.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:dd0e081319328928531df7a0e63621caf67652c8464303fd102141b785ef9547", size = 4530623 }, 26 | { url = "https://files.pythonhosted.org/packages/ff/41/026879e90c84a88e33fb00cc6bd915ac2743c67e87a18f80270dfe3c2041/pillow-11.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e63e4e5081de46517099dc30abe418122f54531a6ae2ebc8680bcd7096860eab", size = 4465191 }, 27 | { url = "https://files.pythonhosted.org/packages/e5/fb/a7960e838bc5df57a2ce23183bfd2290d97c33028b96bde332a9057834d3/pillow-11.1.0-cp313-cp313t-win32.whl", hash = "sha256:dda60aa465b861324e65a78c9f5cf0f4bc713e4309f83bc387be158b077963d9", size = 2295494 }, 28 | { url = "https://files.pythonhosted.org/packages/d7/6c/6ec83ee2f6f0fda8d4cf89045c6be4b0373ebfc363ba8538f8c999f63fcd/pillow-11.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ad5db5781c774ab9a9b2c4302bbf0c1014960a0a7be63278d13ae6fdf88126fe", size = 2631595 }, 29 | { url = "https://files.pythonhosted.org/packages/cf/6c/41c21c6c8af92b9fea313aa47c75de49e2f9a467964ee33eb0135d47eb64/pillow-11.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:67cd427c68926108778a9005f2a04adbd5e67c442ed21d95389fe1d595458756", size = 2377651 }, 30 | ] 31 | 32 | [[package]] 33 | name = "scripts" 34 | version = "0.1.0" 35 | source = { virtual = "." } 36 | dependencies = [ 37 | { name = "pillow" }, 38 | ] 39 | 40 | [package.metadata] 41 | requires-dist = [{ name = "pillow", specifier = ">=11.1.0,<12.0.0" }] 42 | --------------------------------------------------------------------------------