├── .gitignore ├── LICENSE ├── README.md ├── examples ├── bg_jp200.bmp ├── demo_code.py ├── demo_st7796s.py └── jp200_code.py └── screensaver ├── __init__.py ├── boingball_32.bmp ├── dvdlogo_70.bmp ├── toast_48.bmp └── toaster_48.bmp /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Tod Kurt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # circuitpython_screensaver 2 | Do you need a screensaver for CircuitPython? Of course you do 3 | 4 | Demo video of dvdlogo screensaver: 5 | 6 | https://user-images.githubusercontent.com/274093/129969608-a1ea6c81-c9af-4391-923e-143fcaf08a24.mp4 7 | 8 | Demo video of flyingtoasters screensaver: 9 | 10 | https://user-images.githubusercontent.com/274093/129991271-908bda7a-8aca-4b34-a2d6-8dba57bfbeea.mp4 11 | 12 | Demo video of boingball screensaver: 13 | 14 | https://user-images.githubusercontent.com/274093/130105797-5048cdb5-e53a-4bdf-a39a-1e45ab7f0733.mp4 15 | 16 | 17 | For more info, see [this tweet thread](https://twitter.com/todbot/status/1428096525217931264). 18 | 19 | ## Installation 20 | 21 | - Copy the entire `screensaver` directory to your CIRCUITPY drive 22 | - See the `demo_code.py` example (or just copy it over as `code.py`) to see how to use it 23 | 24 | ## Usage 25 | 26 | To load up a screensaver and run the screensaver forever: 27 | 28 | ```py 29 | from screensaver import screensaver_dvdlogo 30 | screensaver_dvdlogo() 31 | ``` 32 | 33 | or 34 | 35 | ```py 36 | from screensaver import screensaver_flyingtoasters 37 | screensaver_flyingtoasters() 38 | 39 | ``` 40 | 41 | or 42 | 43 | ```py 44 | from screensaver import screensaver_boingball 45 | screensaver_boingball() 46 | 47 | ``` 48 | 49 | To make a screensaver stop after a condition is met, pass in a function as the 50 | `should_exit_func` parameter. If this function returns `True` the screensaver 51 | exits. 52 | 53 | For example. this `exit_screensaver()` function returns `True` after 10 seconds: 54 | 55 | ```py 56 | saver_time = time.monotonic() 57 | def exit_screensaver(): 58 | return (time.monotonic() - saver_time > 10) # allow 10 secs of savering 59 | 60 | screensaver_dvdlogo( should_exit_func=exit_screensaver ) 61 | ``` 62 | 63 | ## Notes 64 | 65 | - Assumes CircuitPython 7, but only for `rainbowio`. And should work in CP6 66 | - `screensaver_flyingtoasters()` takes optional `num_toasters` and `num_toasts` arguments to tune how many you want 67 | - `screensaver_boingball()` takes optional `bg_fname` argument for a background image to put behind the ball 68 | 69 | -------------------------------------------------------------------------------- /examples/bg_jp200.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython_screensaver/b30b34531d84fbd1b806b2950be3cc19c72762c9/examples/bg_jp200.bmp -------------------------------------------------------------------------------- /examples/demo_code.py: -------------------------------------------------------------------------------- 1 | # screensaver_demo_code.py -- demonstrate screensaver use 2 | # 17 Aug 2021 3 | 4 | import time # time is money 5 | import board 6 | 7 | from screensaver import screensaver_dvdlogo 8 | 9 | # This is our main loop 10 | # where we do our very important work 11 | while True: 12 | for i in range(20): 13 | print(time.monotonic(),"doing busy work...") 14 | time.sleep(0.3) 15 | 16 | # but now it's time for a break 17 | print("*** now screensavering") 18 | 19 | # how to get out of the screensaver 20 | saver_time = time.monotonic() 21 | def exit_screensaver(): 22 | return (time.monotonic() - saver_time > 10) # allow 10 secs of savering 23 | 24 | screensaver_dvdlogo( should_exit_func=exit_screensaver ) 25 | 26 | # back to work 27 | board.DISPLAY.auto_refresh=True 28 | board.DISPLAY.show(None) 29 | 30 | -------------------------------------------------------------------------------- /examples/demo_st7796s.py: -------------------------------------------------------------------------------- 1 | # screensaver_demo_code.py -- demonstrate screensaver use 2 | # 17 Aug 2021 3 | # 5 Apr 2024 - @DJDevon3 - Demo for non-built-in display using ST7796S 4 | 5 | import time 6 | import board 7 | import displayio 8 | import fourwire 9 | from circuitpython_st7796s import ST7796S 10 | from screensaver import screensaver_dvdlogo 11 | 12 | spi = board.SPI() 13 | tft_cs = board.D9 14 | tft_dc = board.D10 15 | tft_rst = board.D17 16 | 17 | # 3.5" ST7796S Display 18 | DISPLAY_WIDTH = 480 19 | DISPLAY_HEIGHT = 320 20 | 21 | displayio.release_displays() 22 | display_bus = fourwire.FourWire(spi, command=tft_dc, chip_select=tft_cs, reset=tft_rst) 23 | display = ST7796S(display_bus, width=DISPLAY_WIDTH, height=DISPLAY_HEIGHT, rotation=180) 24 | 25 | # This is our main loop 26 | # where we do our very important work 27 | while True: 28 | for i in range(20): 29 | print(time.monotonic(),"doing busy work...") 30 | time.sleep(0.3) 31 | 32 | # but now it's time for a break 33 | print("*** now screensavering") 34 | 35 | # how to get out of the screensaver 36 | saver_time = time.monotonic() 37 | def exit_screensaver(): 38 | return (time.monotonic() - saver_time > 10) # allow 10 secs of savering 39 | 40 | screensaver_dvdlogo(display=display, should_exit_func=exit_screensaver ) 41 | 42 | # back to work 43 | display.auto_refresh=True 44 | -------------------------------------------------------------------------------- /examples/jp200_code.py: -------------------------------------------------------------------------------- 1 | # jp200_code.py -- screensaver celbebration for JP's 200th show 2 | # This is designed for FunHouse but should work on other boards 3 | # 19 Aug 2021 4 | 5 | from screensaver import screensaver_boingball 6 | 7 | screensaver_boingball(bg_fname="bg_jp200.bmp") 8 | 9 | 10 | -------------------------------------------------------------------------------- /screensaver/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # screensaver.py -- screensavers for CircuitPython 3 | # 17 Aug 2021 - @todbot 4 | # 5 | import time, random 6 | import board, displayio 7 | import adafruit_imageload 8 | 9 | try: 10 | import rainbowio 11 | def randcolor(): return rainbowio.colorwheel(random.randint(0,255)) 12 | except ImportError: 13 | def randcolor(): return random.randint(0,0xffffff) # not as good but passable 14 | 15 | # dvdlogo! currently our main screensaver 16 | def screensaver_dvdlogo(display=None, should_exit_func=None): 17 | 18 | sprite_w = 70 # width of the sprite to create 19 | sprite_fname="/screensaver/dvdlogo_70.bmp" 20 | 21 | display.auto_refresh = False # only update display on display.refresh() 22 | screen = displayio.Group() # group that holds everything 23 | display.root_group = screen # add main group to display 24 | 25 | sprite1,sprite1_pal = adafruit_imageload.load(sprite_fname) 26 | sprite1_pal.make_transparent(0) 27 | sprite1_tg = displayio.TileGrid(sprite1, pixel_shader=sprite1_pal) 28 | screen.append(sprite1_tg) 29 | 30 | x, y = display.width/2, display.height/2 # starting position, middle of screen 31 | vx,vy = display.width / 100, display.height / 150 # initial velocity that seems cool 32 | 33 | sprite_hw = sprite_w//2 # integer half-width of our sprite, for bounce detection 34 | 35 | while True: 36 | if should_exit_func is not None and should_exit_func(): return 37 | # update our position based on our velocity 38 | x,y = x + vx, y + vy 39 | # x,y is centered on our sprite, so to check bounds 40 | # add in half-width to get at edges 41 | # a bounce just changes the polarity of the velocity 42 | if x - sprite_hw < 0 or x + sprite_hw > display.width: 43 | vx = -vx # bounce! 44 | sprite1_pal[1] = randcolor() # rainbowio.colorwheel(random.randint(0,255)) 45 | if y - sprite_hw < 0 or y + sprite_hw > display.height: 46 | vy = -vy # bounce! 47 | sprite1_pal[1] = randcolor() # rainbowio.colorwheel(random.randint(0,255)) 48 | # TileGrids are top-left referenced, so subtract that off 49 | # and convert to integer pixel x,y before setting tilegrid xy 50 | sprite1_tg.x = int(x - sprite_hw) 51 | sprite1_tg.y = int(y - sprite_hw) 52 | 53 | # this gives framerate of 20-24 FPS on FunHouse (ESP32S2 240x240 SPI TFT) 54 | display.refresh(); time.sleep(0.01) 55 | # whereas this is jerky: every other frame 11 FPS & 0 FPS, at 20 FPS rate 56 | #display.refresh(target_frames_per_second=20, minimum_frames_per_second=0) 57 | 58 | 59 | # flying toasters! 60 | def screensaver_flyingtoasters(display=None, should_exit_func=None, 61 | num_toasters=2, num_toasts=3): 62 | sprite_w = 48 # width of the sprites 63 | sprite1_fname="/screensaver/toast_48.bmp" 64 | sprite2_fname="/screensaver/toaster_48.bmp" 65 | sprite2_tile_count = 4 66 | 67 | display.auto_refresh = False # only update display on display.refresh() 68 | screen = displayio.Group() # group that holds everything 69 | display.root_group = screen # add main group to display 70 | 71 | sprite1,sprite1_pal = adafruit_imageload.load(sprite1_fname) 72 | sprite1_pal.make_transparent(0) 73 | sprite2,sprite2_pal = adafruit_imageload.load(sprite2_fname) 74 | sprite2_pal.make_transparent(0) 75 | 76 | sprite_hw = sprite_w//2 # integer half-width of our sprite, for bounce detection 77 | 78 | class Sprite: 79 | def __init__(self, tg, x,y, vx,vy, tile_count=1, anim_speed=0): 80 | self.tg = tg 81 | self.x,self.y = x,y 82 | self.vx,self.vy = vx,vy 83 | self.tile_count = tile_count 84 | self.anim_speed = anim_speed 85 | self.last_time = time.monotonic() 86 | def update_pos(self): 87 | self.x = self.x + self.vx 88 | self.y = self.y + self.vy 89 | # TileGrids are top-left referenced, so subtract that off 90 | # and convert to integer pixel x,y before setting tilegrid xy 91 | self.tg.x = int(self.x - sprite_hw) 92 | self.tg.y = int(self.y - sprite_hw) 93 | def next_tile(self): 94 | if self.tile_count == 1: return 95 | if time.monotonic() - self.last_time > self.anim_speed: 96 | self.last_time = time.monotonic() 97 | tilenum = (toaster.tg[0] + 1) % toaster.tile_count 98 | toaster.tg[0] = tilenum 99 | 100 | toasts = [] 101 | for i in range(num_toasts): 102 | x,y = random.randint(0,display.width), random.randint(0,display.height) 103 | vx,vy = -1.4 - random.uniform(0,0.8), 1 # standard toast velocity direction 104 | tg = displayio.TileGrid(sprite1, pixel_shader=sprite1_pal) 105 | sprite = Sprite(tg, x,y, vx,vy, 1) 106 | toasts.append( sprite ) 107 | screen.append(tg) 108 | 109 | toasters = [] 110 | for i in range(num_toasters): 111 | x,y = random.randint(0,display.width), random.randint(0,display.height) 112 | vx,vy = -1.3 - random.random(), 1 # standard toast velocity direction 113 | tg = displayio.TileGrid(sprite2, pixel_shader=sprite2_pal, 114 | width=1, height=1, 115 | tile_width=sprite_w, tile_height=sprite_w) 116 | sprite = Sprite(tg, x,y, vx,vy, tile_count=sprite2_tile_count, anim_speed=0.1) 117 | sprite.tg[0] = random.randint(0, sprite2_tile_count-1) # randomize anim sequence 118 | toasters.append(sprite) 119 | screen.append(tg) 120 | 121 | flap_time = time.monotonic() 122 | while True: 123 | if should_exit_func is not None and should_exit_func(): return 124 | 125 | # update our position based on our velocity 126 | for toast in toasts: 127 | toast.update_pos() 128 | if toast.x < 0 or toast.y > display.height: 129 | toast.x = display.width 130 | toast.y = random.randint(0,display.height)/2 131 | 132 | for toaster in toasters: 133 | toaster.update_pos() 134 | toaster.next_tile() 135 | if toaster.x < 0 or toaster.y > display.height: 136 | toaster.x = display.width 137 | toaster.y = random.randint(0,display.height)/2 138 | toaster.tg[0] = random.randint(0, sprite2_tile_count-1) 139 | 140 | # this gives framerate of 20-24 FPS on FunHouse (ESP32S2 240x240 SPI TFT) 141 | display.refresh(); time.sleep(0.01) 142 | 143 | # boingball! amiga bouncing ball 144 | def screensaver_boingball(display=None, should_exit_func=None, 145 | bg_fname=None): 146 | 147 | sprite_scale = 2 148 | if display.height < 150: sprite_scale = 1 149 | 150 | sprite_w = 32 # width of the sprite to create 151 | sprite_fname="/screensaver/boingball_32.bmp" 152 | sprite_tile_count = 18 153 | 154 | display.auto_refresh = False # only update display on display.refresh() 155 | screen = displayio.Group() # group that holds everything 156 | display.root_group = screen # add main group to display 157 | 158 | # get background image, if there is one 159 | if bg_fname is not None: 160 | bg_img, bg_pal = adafruit_imageload.load(bg_fname) 161 | screen.append(displayio.TileGrid(bg_img, pixel_shader=bg_pal)) 162 | 163 | sprite,sprite_pal = adafruit_imageload.load(sprite_fname) 164 | sprite_pal.make_transparent(0) 165 | sprite_pal.make_transparent(1) 166 | sprite_tg = displayio.TileGrid(sprite, pixel_shader=sprite_pal, 167 | width=1, height=1, 168 | tile_width=sprite_w, tile_height=sprite_w) 169 | sprite = displayio.Group(scale=sprite_scale) 170 | sprite.append(sprite_tg) 171 | screen.append(sprite) 172 | 173 | x, y = display.width/2, display.height/2 # starting position, middle of screen 174 | vx,vy = display.width / 55, display.height / 80 # initial velocity 175 | 176 | sprite_hw = sprite_w//2 * sprite_scale # integer half-width for bounce detection 177 | 178 | g = 0.25 # our gravity acceleration 179 | tile_inc = 1 # which way we play the sprite animation tiles 180 | 181 | last_tile_time = time.monotonic() 182 | while True: 183 | if should_exit_func is not None and should_exit_func(): return 184 | 185 | # update our position based on our velocity 186 | x,y = x + vx, y + vy 187 | # update our velocity based on acceleration 188 | vy = vy + g 189 | # a bounce changes the polarity of the velocity 190 | if x - sprite_hw < 0 or x + sprite_hw > display.width: 191 | vx = -vx # bounce! 192 | tile_inc = - tile_inc # change ball "spinning" direction 193 | if y + sprite_hw > display.height: 194 | vy = -(vy - g) # bounce! (and remove gravity we added before) 195 | 196 | # TileGrids are top-left referenced, so subtract that off 197 | # and convert to integer pixel x,y before setting tilegrid xy 198 | sprite.x = int(x - sprite_hw) 199 | sprite.y = int(y - sprite_hw) 200 | 201 | # do the animation 202 | if time.monotonic() - last_tile_time > 0.01: 203 | last_tile_time = time.monotonic() 204 | # get first thing in group (only thing), assume it's a TileGrid 205 | # then access first space (only gridspace) 206 | sprite[0][0] = (sprite[0][0] + tile_inc) % sprite_tile_count 207 | 208 | # this gives framerate of 20-24 FPS on FunHouse (ESP32S2 240x240 SPI TFT) 209 | display.refresh(); time.sleep(0.01) 210 | 211 | -------------------------------------------------------------------------------- /screensaver/boingball_32.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython_screensaver/b30b34531d84fbd1b806b2950be3cc19c72762c9/screensaver/boingball_32.bmp -------------------------------------------------------------------------------- /screensaver/dvdlogo_70.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython_screensaver/b30b34531d84fbd1b806b2950be3cc19c72762c9/screensaver/dvdlogo_70.bmp -------------------------------------------------------------------------------- /screensaver/toast_48.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython_screensaver/b30b34531d84fbd1b806b2950be3cc19c72762c9/screensaver/toast_48.bmp -------------------------------------------------------------------------------- /screensaver/toaster_48.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/circuitpython_screensaver/b30b34531d84fbd1b806b2950be3cc19c72762c9/screensaver/toaster_48.bmp --------------------------------------------------------------------------------