├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── documentation-issue.md │ └── feature_request.md └── workflows │ ├── formatting.yml │ ├── raspberry_pi.yml │ └── test.yaml ├── .gitignore ├── .pylintrc ├── .readthedocs.yaml ├── .vscode └── settings.json ├── README.md ├── broadcasters ├── __init__.py ├── mqtt.py └── utils.py ├── controllers ├── __init__.py ├── gamepad.py ├── keyboard.py └── mqtt.py ├── demos ├── autotype │ ├── ihaveadream.txt │ └── main.py ├── breakout │ ├── high_score.txt │ └── main.py ├── breakout_ai │ └── main.py ├── camera │ └── _main.py ├── cell │ └── main.py ├── checkerboard │ └── main.py ├── circle │ └── main.py ├── cube │ └── main.py ├── digit_clock │ ├── main.py │ └── trace.py ├── doom │ ├── README.md │ ├── assets │ │ ├── full.wad │ │ ├── fuller.wad │ │ ├── miniwad.wad │ │ └── subvert.wad │ ├── main.py │ └── setup.sh ├── flappy_pixel │ └── main.py ├── game_of_life │ └── main.py ├── hangman │ ├── guess.py │ ├── main.py │ └── trace.py ├── hangman_ai │ ├── guess.py │ ├── main.py │ └── trace.py ├── identify │ └── main.py ├── letters │ └── main.py ├── minesweeper │ └── main.py ├── netlab_flag │ └── main.py ├── rain │ └── main.py ├── sine │ └── main.py ├── snake │ ├── high_score.txt │ ├── main.py │ ├── main_old.py │ ├── snek_ai.py │ └── snek_state.py ├── snake_ai │ ├── ai_high_score.txt │ ├── main.py │ ├── main_old.py │ ├── snek_ai.py │ └── snek_state.py ├── sound_visualizer │ ├── bensound-evolution.npy │ ├── bensound-memories.npy │ ├── bensound-pianomoment.npy │ ├── main.py │ └── song_processing.py ├── spiral │ └── main.py ├── sweep │ └── main.py ├── template │ └── main.py ├── tetris │ └── main.py ├── under_construction │ └── main.py ├── utils.py ├── video │ ├── main.py │ ├── resources │ │ └── pre-processed │ │ │ ├── Avatar.npz │ │ │ ├── Bob_Ross.npz │ │ │ ├── Maxwell.npz │ │ │ ├── Rick.npz │ │ │ ├── Stick_Bug.npz │ │ │ ├── nyan.npz │ │ │ ├── shrek.npz │ │ │ └── sully.npz │ └── video_processing.py ├── welcome_netlab │ ├── main.py │ └── main_prog.py └── welcome_y │ └── main.py ├── display ├── __init__.py ├── display.py ├── physical_screen.py ├── physical_screen_v2.py ├── segment_display.py ├── seven_seg.py ├── seven_seg_v2.py ├── symbols.py ├── virtual_screen.py └── virtual_seven_seg.py ├── docs ├── API │ ├── Broadcasters.md │ ├── Controllers.md │ ├── Display.md │ └── Runners.md ├── Hardware │ ├── Camera.md │ ├── Enclosure and feet.md │ ├── Final product.md │ ├── Panel.md │ ├── Power Board.md │ └── Raspberry Pi IO.md ├── Installation │ ├── Install SSS on Mac or Linux.md │ └── Install SSS on Windows.md ├── News │ ├── Competitions.md │ ├── Conferences.md │ └── SSS in the Wild.md ├── Overview │ ├── About the team.md │ ├── Get started.md │ └── Introduction.md ├── Release Notes.md ├── Simulator │ └── Introduction.md ├── Troubleshooting │ ├── Common issues.md │ └── Support.md ├── Tutorials │ ├── Adding Input Controllers.md │ ├── Adding Output Broadcasters.md │ ├── Creating a demo.md │ └── Game Display (Graphics functions).md ├── assets │ ├── 3MFLoader.js │ ├── OrbitControls.js │ ├── cam-case-sss.3mf │ ├── cam-lid-sss.3mf │ ├── camera-pcb.png │ ├── camera-schematic.png │ ├── doom_video.GIF │ ├── enclosure-back-sss.svg │ ├── enclosure-front-sss.svg │ ├── feet-sss.3mf │ ├── fflate.module.js │ ├── panel-pcb-sss.png │ ├── panel-sch-sss.png │ ├── panel-sss.png │ ├── pi-compet.jpeg │ ├── power-board-pcb-sss.png │ ├── power-board-sch-sss.png │ ├── power-board-sss.png │ ├── pycon.pdf │ ├── pycon2023.jpg │ ├── rpi-io.svg │ ├── snek_video.GIF │ ├── sss-back.jpg │ ├── sss-front.jpg │ ├── sss-lab.jpg │ ├── sss.png │ ├── sss_video.GIF │ └── three.module.js └── index.md ├── dts └── sss.dts ├── hw ├── camera │ ├── cam-case-sss.3mf │ └── cam-lid-sss.3mf ├── case │ ├── 10 Series │ │ ├── Frame Assembly v7.3mf │ │ ├── Frame Assembly v7.f3z │ │ ├── TS10-10 v5.3mf │ │ ├── TS20-50-459 v8.3mf │ │ └── TS20-50-635 v5.3mf │ ├── Brackets │ │ ├── Big Bracket v3.3mf │ │ ├── Panel Holder 1 v4.3mf │ │ ├── Panel Holder 2 v2.3mf │ │ ├── Small Bracket v3.3mf │ │ ├── Spacer 1 v4.3mf │ │ └── Spacer 2 v6.3mf │ ├── Front Acrylic v13.3mf │ ├── Full Assembly v14.3mf │ ├── Full Assembly v14.f3z │ ├── Panels │ │ └── Single Panel v19.3mf │ ├── Pi │ │ ├── BOTTOM BODY v6.3mf │ │ └── TOP BODY v8.3mf │ ├── Power Distribution Board │ │ └── Power Distribution Board Case v10.3mf │ └── Power Supply │ │ ├── Bracket v5.3mf │ │ └── Power Supply Case v11.3mf ├── enclosure │ ├── enclosure-back-sss.svg │ └── enclosure-front-sss.svg ├── panel │ ├── panel-sss.brd │ └── panel-sss.sch └── power_board │ ├── power-board-sss.brd │ └── power-board-sss.sch ├── main.py ├── mkdocs-requirements.txt ├── mkdocs.yml ├── overrides └── main.html ├── requirements.txt └── runners ├── demo.py ├── kiosk.py ├── simulator.py ├── test.py └── utils.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. Ubuntu] 28 | - Browser [e.g. chrome, firefox] 29 | - Version [e.g. 22] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation-issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Documentation issue 3 | about: Describe this issue template's purpose here. 4 | title: '' 5 | labels: documentation 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Location 11 | [Link](provide link to the affected page here) 12 | 13 | ## Issue 14 | Provide a concise description of where you think the documentation is incomplete, ambiguous, erroneous, etc 15 | 16 | ## Suggested Fix 17 | How do you think the documentation should look instead? What could be done to improve upon the issue? 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/formatting.yml: -------------------------------------------------------------------------------- 1 | name: Check formatting 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: ["3.10"] 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Set up Python ${{ matrix.python-version }} 14 | uses: actions/setup-python@v5 15 | with: 16 | python-version: ${{ matrix.python-version }} 17 | - name: Install dependencies 18 | run: | 19 | python -m pip install --upgrade pip 20 | pip install black isort 21 | - name: Analyzing the code with black 22 | run: | 23 | black --check --diff $(git ls-files '*.py') 24 | - name: Analyzing imports with isort 25 | run: | 26 | isort --profile black --check --diff $(git ls-files '*.py') 27 | -------------------------------------------------------------------------------- /.github/workflows/raspberry_pi.yml: -------------------------------------------------------------------------------- 1 | name: Test on Raspberry Pi 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: self-hosted 8 | strategy: 9 | matrix: 10 | python-version: ["3.10", "3.11", "3.12", "3.13"] 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | - name: Install system dependencies 15 | run: | 16 | sudo apt update 17 | sudo apt install -y libatlas-base-dev \ 18 | python3-pip \ 19 | python3-venv \ 20 | python3-dev \ 21 | libffi-dev \ 22 | build-essential \ 23 | libgl1-mesa-glx \ 24 | libsdl2-dev \ 25 | libsdl2-image-dev \ 26 | libsdl2-mixer-dev \ 27 | libsdl2-ttf-dev \ 28 | libjpeg-dev \ 29 | libpng-dev \ 30 | libfreetype6-dev \ 31 | libportmidi-dev \ 32 | 33 | - name: Set up folder 34 | run: | 35 | pwd 36 | rm -rf /tmp/test-${{ matrix.python-version }} 37 | mkdir /tmp/test-${{ matrix.python-version }} 38 | cd /tmp/test-${{ matrix.python-version }} 39 | 40 | - name: Set up Python version and virtual environment 41 | run: | 42 | export PYENV_ROOT="$HOME/.pyenv" 43 | export PATH="$PYENV_ROOT/bin:$PATH" 44 | eval "$(pyenv init --path)" 45 | pyenv local ${{ matrix.python-version }} 46 | python -m venv /tmp/test-${{ matrix.python-version }}/.venv 47 | 48 | - name: Install project dependencies 49 | run: | 50 | /tmp/test-${{ matrix.python-version }}/.venv/bin/python -m pip install -r requirements.txt 51 | 52 | - name: Run all demos to make sure they load 53 | run: | 54 | /tmp/test-${{ matrix.python-version }}/.venv/bin/python main.py test 55 | 56 | - name: Clean up code 57 | if: always() 58 | run: | 59 | rm -rf /tmp/test-${{ matrix.python-version }} 60 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: ["3.10", "3.11", "3.12", "3.13"] 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Set up Python ${{ matrix.python-version }} 14 | uses: actions/setup-python@v5 15 | with: 16 | python-version: ${{ matrix.python-version }} 17 | - name: Install dependencies 18 | run: | 19 | python -m pip install -r requirements.txt 20 | - name: Run all demos to make sure they load 21 | run: | 22 | python main.py test 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | mqtt_config.json 2 | 3 | .idea/ 4 | *venv 5 | #demos/ 6 | *.pyc 7 | pico-go.json 8 | __pycache__ 9 | *.swp 10 | *.log 11 | .venv 12 | .idea/ 13 | *.yaml 14 | !.readthedocs.yaml 15 | *high_score* 16 | chocolate-doom 17 | site -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | disable= 3 | C0114, # missing-module-docstring 4 | 5 | [FORMAT] 6 | good-names=i,j,k,x,y 7 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file for MkDocs projects 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Set the version of Python and other tools you might need 8 | build: 9 | os: ubuntu-22.04 10 | tools: 11 | python: "3.12" 12 | 13 | # Optionally declare the Python requirements required to build your docs 14 | python: 15 | install: 16 | - requirements: mkdocs-requirements.txt 17 | 18 | mkdocs: 19 | configuration: mkdocs.yml 20 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.formatting.provider": "black", 3 | "editor.formatOnSave": true, 4 | "editor.codeActionsOnSave": {} 5 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Seven Segment Sign 2 | 3 | ![Black Formatter 2](https://github.com/NET-BYU/sss/actions/workflows/formatting.yml/badge.svg) ![Docs](https://github.com/NET-BYU/sss/actions/workflows/pages/pages-build-deployment/badge.svg) ![Test](https://github.com/NET-BYU/sss/actions/workflows/test.yaml/badge.svg) ![Raspberry Pi](https://github.com/NET-BYU/sss/actions/workflows/raspberry_pi.yml/badge.svg) [![Documentation Status](https://readthedocs.org/projects/sss/badge/?version=latest)](https://sss.readthedocs.io/en/latest/?badge=latest) 4 | 5 | ## Welcome to the Seven Segment Sign! (SSS) 6 | ![ricky](docs/assets/doom_video.GIF) 7 | 8 | ## Getting Started 9 | Here are some helpful links to get you up and running! 10 | - [API Docs](https://sss.readthedocs.io/en/latest/API/Display/) 11 | - [Install on Linux/Mac](https://sss.readthedocs.io/en/latest/Installation/Install%20SSS%20on%20Mac%20or%20Linux/) 12 | - [Install on Windows](https://sss.readthedocs.io/en/latest/Installation/Install%20SSS%20on%20Windows/) 13 | - [Create your first demo](https://sss.readthedocs.io/en/latest/Tutorials/Creating%20a%20demo/) 14 | 15 | ## Common Commands 16 | Although thorough information on how to [get started](https://sss.readthedocs.io/en/latest/Overview/Get%20started/) is in the docs, here are some quick commands you can run in order to get rolling **once you have everything installed**: 17 | 18 | - Run the PyGame Simulator for the SSS 19 | ```bash 20 | python3 main.py simulator 21 | ``` 22 | - Run a specific demo in the simulator 23 | ```bash 24 | python3 main.py demo -s 25 | ``` 26 | - Start the SSS in kiosk mode 27 | ```bash 28 | python3 main.py kiosk 29 | ``` 30 | - To view log output of varying degrees, use the `-v` option (the more `v`'s the more `v`erbose the output becomes 31 | 32 | For any other help or a list of comprehensive commands, run 33 | ```bash 34 | python3 main.py --help 35 | ``` 36 | 37 | ## Reporting an Issue 38 | We love feedback! As the SSS codespace expands and acquires new features, we realize that some bugs will fall through the cracks. Please feel free to open a new issue if you notice any bugs in the code, mistakes or missing information in the documentation, or if you want a new feature to be added to the project. 39 | 40 | You can go to the **Issues** tab to create a new issue. Please be mindful to choose the correct template for the right type of question. Apart from the currently open issues from before this writing, any **issue that does not conform to the template may become stale or be closed without warning.** 41 | 42 | ## Submitting a PR 43 | If you are feeling creative and you would like to contribute a demo but you are outside of our organization, you are more than welcome to submit a pull request and we will look it over! 44 | 45 | In order for a pull request to be merged, we have the following three policies: 46 | 1. Your code must have adequate, meaningful comments. Don't feel like you have to explain every statement, but also don't write comments like `# This a loop that goes over all the values`. 47 | 2. Your PR will need approval from **at least 2 org members**. Users who respond to these the most are @apal6981, @christopolise, and @philipbl 48 | 3. Your code **MUST** be formatted with `black`. This is as simple as adding the [Black Formatter](https://marketplace.visualstudio.com/items?itemName=ms-python.black-formatter) to VS/OSS Code or running `python3 -m black ` 49 | -------------------------------------------------------------------------------- /broadcasters/__init__.py: -------------------------------------------------------------------------------- 1 | # import this 2 | from queue import Queue 3 | 4 | from loguru import logger 5 | 6 | from . import utils 7 | 8 | 9 | def start_outputs(system_queue, demo_output_queue): 10 | """ 11 | Start the output processing. 12 | 13 | Args: 14 | system_queue (Queue): The system queue. 15 | demo_output_queue (Queue): The demo output queue. 16 | """ 17 | try: 18 | logger.info("Loading MQTT output...") 19 | from . import mqtt 20 | 21 | mqtt_q = Queue() 22 | 23 | mqtt_runner = mqtt.start_processing_output(system_queue, mqtt_q) 24 | logger.info("...done") 25 | except ValueError as e: 26 | mqtt_runner = None 27 | logger.warning(e) 28 | logger.warning("Unable to load config file necessary to run MQTT output.") 29 | logger.warning("Program will continue to run without this output.") 30 | except FileNotFoundError as e: 31 | mqtt_runner = None 32 | logger.warning(e) 33 | logger.warning("Unable to open config file necessary to run MQTT output.") 34 | logger.warning("Program will continue to run without this output.") 35 | except ModuleNotFoundError as e: 36 | mqtt_runner = None 37 | logger.warning(e) 38 | logger.warning("Unable to import modules necessary to run MQTT output.") 39 | logger.warning("Program will continue to run without this output.") 40 | 41 | while True: 42 | for payload in utils.get_all_from_queue(demo_output_queue): 43 | if mqtt_runner: 44 | mqtt_q.put(payload) 45 | next(mqtt_runner) 46 | 47 | yield 48 | -------------------------------------------------------------------------------- /broadcasters/mqtt.py: -------------------------------------------------------------------------------- 1 | import paho.mqtt.client as mqtt 2 | from loguru import logger 3 | from yaml import safe_load 4 | 5 | from . import utils 6 | 7 | 8 | def start_processing_output(system_queue, mqtt_q): 9 | """ 10 | Called by the broadcaster module to initialize a connection to the desired MQTT broker. 11 | 12 | Args: 13 | system_queue (Queue): The queue to put system output events in. 14 | mqtt_q (Queue): The queue to put MQTT messages in. 15 | """ 16 | 17 | def on_connect(client, userdata, flags, rc): 18 | """ 19 | Callback function executed upon the successful connection to the desired broker 20 | 21 | Args: 22 | client (mqtt.Client): The client instance for this callback 23 | userdata (Any): The private user data as set in Client() or user_data_set() 24 | flags (Dict): Response flags sent by the broker 25 | rc (int): The connection result 26 | """ 27 | logger.info("MQTT Client connected ({})", rc) 28 | 29 | def on_disconnect(client, userdata, rc): 30 | """ 31 | Callback function executed upon disconnecting from the broker 32 | 33 | Args: 34 | client (mqtt.Client): The client instance for this callback 35 | userdata (Any): The private user data as set in Client() or user_data_set() 36 | rc (int): The connection result 37 | """ 38 | logger.info("MQTT Client disconnected ({})", rc) 39 | 40 | with open("mqtt_config.yaml") as f: 41 | config = safe_load(f) 42 | 43 | if not config["host"] or not config["port"]: 44 | raise ValueError("mqtt_config.yaml not set up") 45 | 46 | client = mqtt.Client() 47 | client.username_pw_set(config["username"], config["password"]) 48 | 49 | if config["tls"]: 50 | client.tls_set() 51 | 52 | client.on_connect = on_connect 53 | client.on_disconnect = on_disconnect 54 | 55 | def process(): 56 | """ 57 | Contains generator which fetches all messages from the `byu_sss/output` topic on broker 58 | 59 | Returns: 60 | Generator: Generator that fetches all messages from the `byu_sss/output` topic on broker 61 | """ 62 | while True: 63 | try: 64 | client.connect(config["host"], config["port"]) 65 | 66 | while True: 67 | client.loop(timeout=0.01) 68 | 69 | for item in utils.get_all_from_queue(mqtt_q): 70 | client.publish( 71 | "byu_sss/output", 72 | payload=str(item), 73 | ) 74 | 75 | yield 76 | 77 | except ConnectionRefusedError: 78 | logger.warning("Unable to connect to broker... trying again later.") 79 | 80 | for _ in range(100): 81 | yield 82 | continue 83 | 84 | return process() 85 | -------------------------------------------------------------------------------- /broadcasters/utils.py: -------------------------------------------------------------------------------- 1 | def get_all_from_queue(queue): 2 | """ 3 | Helper function that gets all items from a given queue. 4 | 5 | Args: 6 | queue (Queue): The queue to get all items from. 7 | """ 8 | while not queue.empty(): 9 | yield queue.get() 10 | -------------------------------------------------------------------------------- /controllers/__init__.py: -------------------------------------------------------------------------------- 1 | from loguru import logger 2 | 3 | 4 | def start_inputs(system_queue, demo_input_queue): 5 | """ 6 | Start the input processing. 7 | 8 | Args: 9 | system_queue (Queue): The system queue. 10 | demo_input_queue (Queue): The demo input queue. 11 | """ 12 | try: 13 | logger.info("Loading MQTT input...") 14 | from . import mqtt 15 | 16 | mqtt_runner = mqtt.start_processing_input(system_queue, demo_input_queue) 17 | logger.info("...done") 18 | except ValueError as e: 19 | mqtt_runner = None 20 | logger.warning(e) 21 | logger.warning("Unable to load config file necessary to run MQTT input.") 22 | logger.warning("Program will continue to run without this input.") 23 | except FileNotFoundError as e: 24 | mqtt_runner = None 25 | logger.warning(e) 26 | logger.warning("Unable to open config file necessary to run MQTT input.") 27 | logger.warning("Program will continue to run without this input.") 28 | except ModuleNotFoundError as e: 29 | mqtt_runner = None 30 | logger.warning(e) 31 | logger.warning("Unable to import modules necessary to run MQTT input.") 32 | logger.warning("Program will continue to run without this input.") 33 | 34 | try: 35 | logger.info("Loading gamepad input...") 36 | from . import gamepad 37 | 38 | gamepad_runner = gamepad.start_processing_input(system_queue, demo_input_queue) 39 | logger.info("...done") 40 | except ModuleNotFoundError as e: 41 | gamepad_runner = None 42 | logger.warning(e) 43 | logger.warning("Unable to import modules necessary to run gamepad input.") 44 | logger.warning("Program will continue to run without this input.") 45 | 46 | try: 47 | logger.info("Loading keyboard input...") 48 | from . import keyboard 49 | 50 | keyboard_runner = None 51 | if keyboard.check_if_sim(): 52 | keyboard_runner = keyboard.start_processing_input( 53 | system_queue, demo_input_queue 54 | ) 55 | logger.info("...done") 56 | except (ImportError, ModuleNotFoundError) as e: 57 | keyboard_runner = None 58 | logger.warning(e) 59 | logger.warning("Unable to import modules necessary to run keyboard input.") 60 | logger.warning("Program will continue to run without this input.") 61 | 62 | while True: 63 | if mqtt_runner: 64 | logger.trace("Fetch MQTT") 65 | next(mqtt_runner) 66 | 67 | if gamepad_runner: 68 | logger.trace("Fetch GamePad") 69 | next(gamepad_runner) 70 | 71 | if keyboard_runner: 72 | logger.trace("Fetch Keyboard") 73 | next(keyboard_runner) 74 | 75 | yield 76 | -------------------------------------------------------------------------------- /controllers/gamepad.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import time 3 | from queue import Empty, Queue 4 | 5 | import inputs 6 | from loguru import logger 7 | 8 | 9 | def get_input_from_gamepad(queue): 10 | """ 11 | Reads input from the gamepad and puts it in the queue. 12 | 13 | Args: 14 | queue (Queue): The queue to put the input events in. 15 | """ 16 | while True: 17 | # See if the gamepad is plugged in 18 | try: 19 | gamepad = inputs.devices.gamepads[0] 20 | except IndexError: 21 | logger.warning("There is no controller plugged in. Trying again later...") 22 | time.sleep(5) 23 | # continue 24 | # TODO: Hotplug gamepad 25 | return None 26 | 27 | # If it is plugged in, get events from it 28 | while True: 29 | for event in gamepad.read(): 30 | queue.put(event) 31 | 32 | 33 | def start_processing_input(system_queue, demo_input_queue): 34 | """ 35 | Listens to input on the device and puts it into the appropriate queue. 36 | 37 | Args: 38 | system_queue (Queue): The queue to put system input events in. 39 | demo_input_queue (Queue): The queue to put demo input events in. 40 | """ 41 | queue = Queue() 42 | 43 | thread = threading.Thread(target=get_input_from_gamepad, args=(queue,), daemon=True) 44 | thread.start() 45 | 46 | def process(): 47 | """ 48 | Contains generator which fetches all messages from the `byu_sss/output` topic on broker 49 | 50 | Returns: 51 | Generator: Generator that fetches all messages from the `byu_sss/output` topic on broker 52 | """ 53 | leftPressed = False 54 | rightPressed = False 55 | upPressed = False 56 | downPressed = False 57 | 58 | # TODO: Handle start and select in system queue? 59 | while True: 60 | while not queue.empty(): 61 | try: 62 | event = queue.get(block=False) 63 | except Empty: 64 | break 65 | 66 | if event.code == "ABS_X": 67 | if event.state == 0: 68 | demo_input_queue.put("LEFT_P") 69 | leftPressed = True 70 | elif event.state == 127: 71 | if leftPressed: 72 | demo_input_queue.put("LEFT_R") 73 | leftPressed = False 74 | elif rightPressed: 75 | demo_input_queue.put("RIGHT_R") 76 | rightPressed = False 77 | elif event.state == 255: 78 | demo_input_queue.put("RIGHT_P") 79 | rightPressed = True 80 | elif event.code == "ABS_Y": 81 | if event.state == 0: 82 | demo_input_queue.put("UP_P") 83 | upPressed = True 84 | elif event.state == 127: 85 | if upPressed: 86 | demo_input_queue.put("UP_R") 87 | upPressed = False 88 | elif downPressed: 89 | demo_input_queue.put("DOWN_R") 90 | downPressed = False 91 | elif event.state == 255: 92 | demo_input_queue.put("DOWN_P") 93 | downPressed = True 94 | elif event.code == "BTN_BASE4": 95 | if event.state: 96 | demo_input_queue.put("START_P") 97 | else: 98 | demo_input_queue.put("START_R") 99 | elif event.code == "BTN_BASE3": 100 | if event.state: 101 | demo_input_queue.put("SEL_P") 102 | else: 103 | demo_input_queue.put("SEL_R") 104 | elif event.code == "BTN_THUMB": 105 | if event.state: 106 | demo_input_queue.put("PRI_P") 107 | else: 108 | demo_input_queue.put("PRI_R") 109 | elif event.code == "BTN_THUMB2": 110 | if event.state: 111 | demo_input_queue.put("SEC_P") 112 | else: 113 | demo_input_queue.put("SEC_R") 114 | yield 115 | 116 | return process() 117 | -------------------------------------------------------------------------------- /controllers/keyboard.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | import pygame_widgets 3 | from loguru import logger 4 | from pygame.locals import ( 5 | K_DOWN, 6 | K_ESCAPE, 7 | K_LEFT, 8 | K_RCTRL, 9 | K_RETURN, 10 | K_RIGHT, 11 | K_SPACE, 12 | K_UP, 13 | KEYDOWN, 14 | KEYUP, 15 | QUIT, 16 | K_n, 17 | ) 18 | 19 | 20 | def check_if_sim(): 21 | """ 22 | Check if the program is running in simulator mode. 23 | 24 | Returns: 25 | bool: True if running in simulator mode, False otherwise. 26 | """ 27 | try: 28 | pygame.event.get() 29 | return True 30 | except pygame.error: 31 | logger.warning("Not running in simulator mode. The keyboard has been disabled.") 32 | return False 33 | 34 | 35 | def start_processing_input(system_queue, demo_input_queue): 36 | """ 37 | Listens to input on the device and puts it into the appropriate queue. 38 | 39 | Args: 40 | system_queue (Queue): The queue to put system input events in. 41 | demo_input_queue (Queue): The queue to put demo input events in. 42 | """ 43 | while True: 44 | events = pygame.event.get() 45 | 46 | for event in events: 47 | # Check for KEYDOWN event and pass into input queue 48 | if event.type == KEYDOWN: 49 | # If the Esc key is pressed, then exit the main loop 50 | if event.key == K_ESCAPE: 51 | system_queue.put("QUIT") 52 | elif event.key == K_LEFT: 53 | demo_input_queue.put("LEFT_P") 54 | elif event.key == K_UP: 55 | demo_input_queue.put("UP_P") 56 | elif event.key == K_RIGHT: 57 | demo_input_queue.put("RIGHT_P") 58 | elif event.key == K_DOWN: 59 | demo_input_queue.put("DOWN_P") 60 | elif event.key == K_RETURN: 61 | demo_input_queue.put("START_P") 62 | elif event.key == K_SPACE: 63 | demo_input_queue.put("PRI_P") 64 | elif event.key == K_n: 65 | demo_input_queue.put("SEC_P") 66 | elif event.key == K_RCTRL: 67 | demo_input_queue.put("SEL_P") 68 | 69 | # check for KEYUP event and pass into input queue 70 | elif event.type == KEYUP: 71 | if event.key == K_LEFT: 72 | demo_input_queue.put("LEFT_R") 73 | elif event.key == K_UP: 74 | demo_input_queue.put("UP_R") 75 | elif event.key == K_RIGHT: 76 | demo_input_queue.put("RIGHT_R") 77 | elif event.key == K_DOWN: 78 | demo_input_queue.put("DOWN_R") 79 | elif event.key == K_RETURN: 80 | demo_input_queue.put("START_R") 81 | elif event.key == K_SPACE: 82 | demo_input_queue.put("PRI_R") 83 | elif event.key == K_n: 84 | demo_input_queue.put("SEC_R") 85 | elif event.key == K_RCTRL: 86 | demo_input_queue.put("SEL_R") 87 | 88 | # Check for QUIT event. 89 | elif event.type == QUIT: 90 | system_queue.put("QUIT") 91 | 92 | pygame_widgets.update(events) 93 | yield 94 | -------------------------------------------------------------------------------- /controllers/mqtt.py: -------------------------------------------------------------------------------- 1 | import json 2 | import socket 3 | 4 | import paho.mqtt.client as mqtt 5 | from loguru import logger 6 | from yaml import safe_load 7 | 8 | 9 | def start_processing_input(system_queue, demo_input_queue): 10 | """ 11 | Listens to input on the device and puts it into the appropriate queue. 12 | 13 | Args: 14 | system_queue (Queue): The queue to put system input events in. 15 | demo_input_queue (Queue): The queue to put demo input events in. 16 | """ 17 | 18 | def on_message(client, userdata, message): 19 | """ 20 | Callback function executed upon receiving a message from the broker 21 | 22 | Args: 23 | client (mqtt.Client): The client instance for this callback 24 | userdata (Any): The private user data as set in Client() or user_data_set() 25 | message (mqtt.MQTTMessage): The message received from the broker 26 | """ 27 | logger.debug("on_message triggered") 28 | try: 29 | data = json.loads(message.payload) 30 | msg_type = data["type"] 31 | msg_input = data["input"] 32 | 33 | if msg_type == "system": 34 | logger.debug("Putting message into system queue: {}", msg_input) 35 | system_queue.put(msg_input) 36 | elif msg_type == "demo": 37 | logger.debug("Putting message into input queue: {}", msg_input) 38 | demo_input_queue.put(msg_input) 39 | else: 40 | logger.error("Unknown MQTT message type: {}", msg_type) 41 | 42 | except json.decoder.JSONDecodeError: 43 | logger.error("MQTT message was not json.") 44 | except KeyError: 45 | logger.error("MQTT message did not have correct keys.") 46 | 47 | def on_connect(client, userdata, flags, rc): 48 | """ 49 | Callback function executed upon the successful connection to the desired broker 50 | 51 | Args: 52 | client (mqtt.Client): The client instance for this callback 53 | userdata (Any): The private user data as set in Client() or user_data_set() 54 | flags (Dict): Response flags sent by the broker 55 | rc (int): The connection result 56 | """ 57 | logger.info("MQTT Client connected ({})", rc) 58 | 59 | # Subscribing in on_connect() means that if we lose the connection and 60 | # reconnect then subscriptions will be renewed. 61 | client.subscribe("byu_sss/input") 62 | 63 | def on_disconnect(client, userdata, rc): 64 | """ 65 | Callback function executed upon disconnecting from the broker 66 | 67 | Args: 68 | client (mqtt.Client): The client instance for this callback 69 | userdata (Any): The private user data as set in Client() or user_data_set() 70 | rc (int): The connection result 71 | """ 72 | logger.info("MQTT Client disconnected ({})", rc) 73 | 74 | with open("mqtt_config.yaml") as f: 75 | config = safe_load(f) 76 | 77 | if not config["host"] or not config["port"]: 78 | raise ValueError("mqtt_config.yaml not set up") 79 | 80 | client = mqtt.Client() 81 | client.username_pw_set(config["username"], config["password"]) 82 | 83 | if config["tls"]: 84 | client.tls_set() 85 | 86 | client.on_message = on_message 87 | client.on_connect = on_connect 88 | client.on_disconnect = on_disconnect 89 | 90 | def process(): 91 | """ 92 | Contains generator which fetches all messages from the `byu_sss/input` topic on broker 93 | 94 | Returns: 95 | Generator: Generator that fetches all messages from the `byu_sss/input` topic on broker 96 | """ 97 | logger.debug("In process func") 98 | while True: 99 | try: 100 | logger.debug("Connecting to broker") 101 | client.connect(config["host"], config["port"]) 102 | logger.debug("...done") 103 | 104 | while True: 105 | client.loop(timeout=0.01) 106 | yield 107 | 108 | except (ConnectionRefusedError, socket.gaierror, Exception) as e: 109 | logger.warning("Unable to connect to broker... trying again later.") 110 | 111 | for _ in range(100): 112 | yield 113 | continue 114 | 115 | return process() 116 | -------------------------------------------------------------------------------- /demos/autotype/main.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | class Autotype: 5 | """This is the autotype demo. It will type out a text file on the screen""" 6 | 7 | demo_time = None 8 | 9 | # User input is passed through input_queue 10 | # Game output is passed through output_queue 11 | # Screen updates are done through the screen object 12 | def __init__(self, input_queue, output_queue, screen): 13 | """ 14 | Constructor 15 | 16 | Args: 17 | input_queue (Queue): Queue for user input 18 | output_queue (Queue): Queue for game output 19 | screen (Screen): Screen object 20 | """ 21 | # Provide the framerate in frames/seconds and the amount of time of the demo in seconds 22 | self.frame_rate = 20 23 | 24 | self.input_queue = input_queue 25 | self.output_queue = output_queue 26 | self.screen = screen 27 | # init demo/game specific variables here 28 | 29 | def run(self): 30 | """Run the demo""" 31 | # Create generator here 32 | 33 | x_cursor = 0 34 | y_cursor = 0 35 | 36 | pixel_vals = np.zeros((self.screen.x_width, self.screen.y_height)) 37 | 38 | nephi = "And it came to pass that I, Nephi, said unto my father: I will go and do the things which the Lord hath commanded, for I know that the Lord giveth no commandments unto the children of men, save he shall prepare a way for them that they may accomplish the thing which he commandeth them." 39 | preamble = "We the People of the United States, in Order to form a more perfect Union, establish Justice, insure domestic Tranquility, provide for the common defense, promote the general Welfare, and secure the Blessings of Liberty to ourselves and our Posterity, do ordain and establish this Constitution for the United States of America." 40 | 41 | with open("demos/autotype/ihaveadream.txt") as file: 42 | dream = file.read() 43 | 44 | typed_text = dream.upper().split() 45 | 46 | while True: 47 | for word in typed_text: 48 | if self.screen.x_width - x_cursor < len(word): 49 | x_cursor = 0 50 | y_cursor += 2 51 | if y_cursor >= self.screen.y_height: 52 | y_cursor -= 2 53 | 54 | # print(self.screen.get_pixel(0, 0)) 55 | # print(self.screen.get_pixel(0, 1)) 56 | 57 | # print(self.screen.get_pixel(1, 0)) 58 | # print(self.screen.get_pixel(1, 1)) 59 | 60 | for y in range(self.screen.y_height): 61 | for x in range(self.screen.x_width): 62 | pixel_vals[y][x] = self.screen.get_pixel(x, y) 63 | 64 | pixel_vals = np.delete(pixel_vals, 0, 0) 65 | pixel_vals = np.delete(pixel_vals, 0, 0) 66 | pixel_vals = np.append( 67 | pixel_vals, [np.zeros(self.screen.x_width)], 0 68 | ) 69 | pixel_vals = np.append( 70 | pixel_vals, [np.zeros(self.screen.x_width)], 0 71 | ) 72 | 73 | self.screen.clear() 74 | for y in range(self.screen.y_height): 75 | for x in range(self.screen.x_width): 76 | self.screen.draw_pixel(x, y, int(pixel_vals[y][x])) 77 | self.screen.push() 78 | yield 79 | for char in word: 80 | self.screen.draw_text(x_cursor, y_cursor, char, push=True) 81 | x_cursor += 1 82 | yield 83 | if self.screen.x_width - x_cursor > 0: 84 | self.screen.draw_text(x_cursor, y_cursor, " ", push=True) 85 | x_cursor += 1 86 | yield 87 | 88 | def stop(self): 89 | """Reset the state of the demo if needed, else leave blank""" 90 | pass 91 | 92 | def get_input_buff(self): 93 | """ 94 | Return the input buffer 95 | 96 | Returns: 97 | list: List of input buffer 98 | """ 99 | return list(self.input_queue.queue) 100 | -------------------------------------------------------------------------------- /demos/breakout/high_score.txt: -------------------------------------------------------------------------------- 1 | 400 -------------------------------------------------------------------------------- /demos/cell/main.py: -------------------------------------------------------------------------------- 1 | import math 2 | from dataclasses import dataclass 3 | 4 | import numpy as np 5 | from perlin_noise import PerlinNoise 6 | 7 | from display.segment_display import SegmentDisplay 8 | 9 | 10 | @dataclass 11 | class Point: 12 | x: float 13 | y: float 14 | 15 | 16 | class Cell: 17 | """ 18 | This simulates a cell moving around. It is a polygon that moves around to 19 | different points in a random fashion. 20 | """ 21 | 22 | demo_time = 30 23 | 24 | def __init__(self, input_queue, output_queue, screen): 25 | """ 26 | Constructor 27 | 28 | Args: 29 | input_queue (Queue): The input queue 30 | output_queue (Queue): The output queue 31 | screen (Screen): The screen to draw on 32 | """ 33 | self.frame_rate = 10 34 | 35 | self.input_queue = input_queue 36 | self.output_queue = output_queue 37 | self.screen = screen 38 | 39 | self.display = SegmentDisplay(self.screen) 40 | 41 | self.num_cell_points = 10 42 | 43 | def run(self): 44 | """Runs the simulation loop""" 45 | count = 0 46 | noise = PerlinNoise() 47 | 48 | while True: 49 | pos = count * 0.025 50 | 51 | # Create cell 52 | cell = [] 53 | for i in range(self.num_cell_points + 1): 54 | sin = math.sin(math.pi * 2 * i / self.num_cell_points + pos) 55 | cos = math.cos(math.pi * 2 * i / self.num_cell_points + pos) 56 | 57 | x = ( 58 | self.display.width / 2 59 | + self.display.width * 0.4 * sin 60 | + self.display.width 61 | * 0.15 62 | * self._map(noise([sin * 5 * 0.5, 0, pos]), 0.2, 0.75, -1, 1) 63 | + 30 64 | ) 65 | 66 | y = ( 67 | self.display.height / 2 68 | + self.display.height * 0.4 * cos 69 | + self.display.height 70 | * 0.15 71 | * self._map(noise([sin * 5 * 0.5, 1, pos]), 0.2, 0.75, -1, 1) 72 | + 20 73 | ) 74 | 75 | cell.append(Point(x, y)) 76 | 77 | for i in range(self.num_cell_points): 78 | self.display.draw_line( 79 | int(cell[i].x), 80 | int(cell[i].y), 81 | int(cell[i + 1].x), 82 | int(cell[i + 1].y), 83 | ) 84 | 85 | self.display.draw_line( 86 | int(cell[self.num_cell_points - 1].x), 87 | int(cell[self.num_cell_points - 1].y), 88 | int(cell[0].x), 89 | int(cell[0].y), 90 | ) 91 | 92 | self.display.draw() 93 | yield 94 | self.display.undraw() 95 | 96 | count += 1 97 | 98 | def _map(self, x, in_min, in_max, out_min, out_max): 99 | """ 100 | Maps a value from one range to another 101 | 102 | Args: 103 | x (float): The value to map 104 | in_min (float): The minimum value of the input range 105 | in_max (float): The maximum value of the input range 106 | out_min (float): The minimum value of the output range 107 | out_max (float): The maximum value of the output range 108 | 109 | Returns: 110 | float: The mapped value 111 | """ 112 | return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min 113 | 114 | def stop(self): 115 | """Reset the state of the demo if needed, else leave blank""" 116 | pass 117 | -------------------------------------------------------------------------------- /demos/checkerboard/main.py: -------------------------------------------------------------------------------- 1 | class Checkerboard: 2 | """This is the checkboard demo. It just alternates a checker pattern on the display""" 3 | 4 | demo_time = None 5 | 6 | # User input is passed through input_queue 7 | # Game output is passed through output_queue 8 | # Screen updates are done through the screen object 9 | def __init__(self, input_queue, output_queue, screen): 10 | """ 11 | Constructor 12 | 13 | Args: 14 | input_queue (Queue): The input queue 15 | output_queue (Queue): The output queue 16 | screen (Screen): The screen to draw on 17 | 18 | """ 19 | # Provide the framerate in frames/seconds and the amount of time of the demo in seconds 20 | self.frame_rate = 5 21 | 22 | self.input_queue = input_queue 23 | self.output_queue = output_queue 24 | self.screen = screen 25 | # init demo/game specific variables here 26 | 27 | def run(self): 28 | """Runs the simulation loop""" 29 | # Create generator here 30 | while True: 31 | for x in range(self.screen.x_width): 32 | for y in range(self.screen.y_height): 33 | if y % 2 == 0: 34 | if x % 2: 35 | self.screen.draw_pixel(x, y, 0xF, combine=False, push=False) 36 | else: 37 | if x % 2 == 0: 38 | self.screen.draw_pixel(x, y, 0xF, combine=False, push=False) 39 | self.screen.push() 40 | yield 41 | 42 | for x in range(self.screen.x_width): 43 | for y in range(self.screen.y_height): 44 | if y % 2: 45 | if x % 2: 46 | self.screen.draw_pixel(x, y, 0xF, combine=False, push=False) 47 | else: 48 | if x % 2 == 0: 49 | self.screen.draw_pixel(x, y, 0xF, combine=False, push=False) 50 | self.screen.push() 51 | yield 52 | 53 | def stop(self): 54 | """Reset the state of the demo if needed, else leave blank""" 55 | pass 56 | -------------------------------------------------------------------------------- /demos/circle/main.py: -------------------------------------------------------------------------------- 1 | class Circle: 2 | """This is a demo that just runs a circle around the perimeter""" 3 | 4 | demo_time = None 5 | 6 | # User input is passed through input_queue 7 | # Game output is passed through output_queue 8 | # Screen updates are done through the screen object 9 | def __init__(self, input_queue, output_queue, screen): 10 | """ 11 | Constructor 12 | 13 | Args: 14 | input_queue (Queue): The input queue 15 | output_queue (Queue): The output queue 16 | screen (Screen): The screen to draw on 17 | """ 18 | # Provide the framerate in frames/seconds and the amount of time of the demo in seconds 19 | self.frame_rate = 30 20 | 21 | self.input_queue = input_queue 22 | self.output_queue = output_queue 23 | self.screen = screen 24 | # init demo/game specific variables here 25 | 26 | def run(self): 27 | """Runs the simulation loop""" 28 | # Create generator here 29 | width = self.screen.x_width 30 | height = self.screen.y_height 31 | while True: 32 | for x in range(1, width): 33 | self.screen.draw_pixel(x - 1, 0, 0x0, push=True) 34 | self.screen.draw_pixel(x, 0, 0xF, push=True) 35 | yield 36 | for y in range(1, height): 37 | self.screen.draw_pixel(width - 1, y - 1, 0x0, push=True) 38 | self.screen.draw_pixel(width - 1, y, 0xF, push=True) 39 | yield 40 | for x in range(width - 1, 0, -1): 41 | self.screen.draw_pixel(x - 1, height - 1, 0xF, combine=False, push=True) 42 | self.screen.draw_pixel(x, height - 1, 0x0, combine=False, push=True) 43 | yield 44 | for y in range(height - 1, 0, -1): 45 | self.screen.draw_pixel(0, y - 1, 0xF, combine=False, push=True) 46 | self.screen.draw_pixel(0, y, 0x0, combine=False, push=True) 47 | yield 48 | 49 | def stop(self): 50 | """Reset the state of the demo if needed, else leave blank""" 51 | pass 52 | -------------------------------------------------------------------------------- /demos/cube/main.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import numpy as np 4 | 5 | from display.segment_display import SegmentDisplay 6 | 7 | 8 | class Cube: 9 | """ 10 | Shows an outline of a cube moving in three dimensions. 11 | """ 12 | 13 | demo_time = 30 14 | 15 | def __init__(self, input_queue, output_queue, screen): 16 | """ 17 | Constructor 18 | 19 | Args: 20 | input_queue (Queue): The input queue 21 | output_queue (Queue): The output queue 22 | screen (Screen): The screen to draw on 23 | """ 24 | self.frame_rate = 50 25 | 26 | self.input_queue = input_queue 27 | self.output_queue = output_queue 28 | self.screen = screen 29 | 30 | self.display = SegmentDisplay(self.screen) 31 | 32 | def run(self): 33 | """Runs the simulation loop""" 34 | rotation_speed = 0.5 35 | angle = 0 36 | points_count = 16 37 | size = 12 38 | 39 | cube = [ 40 | [-size, -size, -size], 41 | [size, -size, -size], 42 | [size, size, -size], 43 | [-size, size, -size], 44 | [-size, -size, -size], 45 | [-size, -size, size], 46 | [size, -size, size], 47 | [size, size, size], 48 | [-size, size, size], 49 | [-size, -size, size], 50 | [-size, size, size], 51 | [-size, size, -size], 52 | [size, size, -size], 53 | [size, size, size], 54 | [size, -size, size], 55 | [size, -size, -size], 56 | ] 57 | 58 | while True: 59 | angle += 0.1 + 3 * rotation_speed 60 | radiansX = math.radians(angle) 61 | radiansY = math.radians(angle) 62 | radiansZ = math.radians(angle) 63 | 64 | points2d = [] 65 | 66 | for i in range(points_count): 67 | # rotateY 68 | rotz = cube[i][2] * math.cos(radiansY) - cube[i][0] * math.sin(radiansY) 69 | rotx = cube[i][2] * math.sin(radiansY) + cube[i][0] * math.cos(radiansY) 70 | roty = cube[i][1] 71 | 72 | # rotateX 73 | rotyy = roty * math.cos(radiansX) - rotz * math.sin(radiansX) 74 | rotzz = roty * math.sin(radiansX) + rotz * math.cos(radiansX) 75 | rotxx = rotx 76 | 77 | # rotateZ 78 | rotxxx = rotxx * math.cos(radiansZ) - rotyy * math.sin(radiansZ) 79 | rotyyy = rotxx * math.sin(radiansZ) + rotyy * math.cos(radiansZ) 80 | rotzzz = rotzz 81 | 82 | # perspective projection 83 | d = 30.0 84 | rotxxx = rotxxx * d / (d - rotzzz) + self.display.width / 2 85 | rotyyy = rotyyy * d / (d - rotzzz) + self.display.height / 2 86 | 87 | points2d.append([int(rotxxx), int(rotyyy)]) 88 | 89 | for i in range(1, points_count): 90 | Sx = points2d[i - 1][0] 91 | Sy = points2d[i - 1][1] 92 | Ex = points2d[i][0] 93 | Ey = points2d[i][1] 94 | 95 | if Sx <= 0: 96 | Sx = 0 97 | if Sx >= 127: 98 | Sx = self.display.width 99 | 100 | if Sy <= 0: 101 | Sy = 0 102 | if Sy >= 63: 103 | Sy = self.display.height 104 | 105 | if Ex <= 0: 106 | Ex = 0 107 | if Ex >= 127: 108 | Ex = self.display.width 109 | 110 | if Ey <= 0: 111 | Ey = 0 112 | if Ey >= 63: 113 | Ey = self.display.height 114 | 115 | self.display.draw_line(Sx, Sy, Ex, Ey) 116 | 117 | self.display.draw() 118 | yield 119 | self.display.undraw() 120 | 121 | def stop(self): 122 | """Reset the state of the demo if needed, else leave blank""" 123 | pass 124 | -------------------------------------------------------------------------------- /demos/digit_clock/main.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from demos.digit_clock.trace import Trace 4 | 5 | 6 | class DigitClock: 7 | """This is the Digital Clock demo. It will display the current time on the screen in a digital format.""" 8 | 9 | demo_time = 30 10 | 11 | def __init__(self, input_queue, output_queue, screen): 12 | """ 13 | Constructor 14 | 15 | Args: 16 | input_queue (Queue): The input queue 17 | output_queue (Queue): The output queue 18 | screen (Screen): The screen to draw on 19 | """ 20 | self.frame_rate = 20 21 | 22 | self.input_queue = input_queue 23 | self.output_queue = output_queue 24 | self.screen = screen 25 | # init demo/game specific variables here 26 | 27 | def run(self): 28 | """Runs the demo loop""" 29 | # Draw two dots in the middle of the screen 30 | self.screen.draw_pixel(24, 22, 15, True, False) 31 | self.screen.draw_pixel(24, 26, 15, True, False) 32 | self.screen.push() 33 | trace = Trace(self.screen) 34 | 35 | def draw_time(current_time, draw): 36 | """ 37 | Draw the current time on the screen 38 | 39 | Args: 40 | current_time (tuple): The current time 41 | draw (bool): If True then draw the time, else clear the time 42 | """ 43 | hour = current_time[3] 44 | min = current_time[4] 45 | if len(str(min)) == 1: 46 | trace.draw_number(0, min, draw) 47 | trace.draw_number(1, 0, draw) 48 | if len(str(min)) == 2: 49 | trace.draw_number(0, int(str(min)[1]), draw) 50 | trace.draw_number(1, int(str(min)[0]), draw) 51 | 52 | if hour > 12: 53 | hour = hour - 12 54 | 55 | if len(str(hour)) == 1: 56 | trace.draw_number(2, hour, draw) 57 | 58 | if len(str(hour)) == 2: 59 | trace.draw_number(2, int(str(hour)[1]), draw) 60 | trace.draw_number(3, int(str(hour)[0]), draw) 61 | 62 | # Initialize current time on the clock 63 | current_time = time.localtime(None) 64 | draw_time(current_time, True) 65 | 66 | # Generator that will compare a new time with the current time and if there is a difference then the generator will update the clock 67 | while True: 68 | new_time = time.localtime(None) 69 | if new_time[4] is not current_time[4]: 70 | draw_time(current_time, False) 71 | draw_time(new_time, True) 72 | current_time = new_time 73 | yield 74 | 75 | def stop(self): 76 | """Reset the state of the demo if needed, else leave blank""" 77 | pass 78 | 79 | def get_input_buff(self): 80 | """Get all input off the queue 81 | 82 | Returns: 83 | list: A list of all the input from the queue 84 | """ 85 | return list(self.input_queue.queue) 86 | -------------------------------------------------------------------------------- /demos/doom/README.md: -------------------------------------------------------------------------------- 1 | # Doom for SSS 2 | 3 | In order to play doom on the SSS, you'll need to make sure that the correct version of the game is installed. Running `setup.sh` should take care of everything **assuming that you have the dev version of SDL installed correctly**. 4 | 5 | On Raspbian, you can install those dependencies by: 6 | 7 | ```bash 8 | sudo apt install libsdl2-dev libsdl2-mixer-dev libsdl2-net-dev 9 | ``` 10 | 11 | # Installation via Script 12 | 13 | To run the installation script, make sure that you **run it from the SSS root directory**: 14 | 15 | ```bash 16 | ./setup.sh 17 | ``` 18 | If everything exited correctly with no errors and you can see `chocolate-doom` in the `doom` demo folder, that means everything ran correctly and you are good to go! 19 | 20 | Because this script is still rough, it is possible that you may encounter errors. In this case, please copy the output of your attempt and [open an issue](https://github.com/NET-BYU/sss/issues/new) paste it into the issue so it can be addressed. Then move on to the manual installation and try to identify which step the script fails on. 21 | 22 | # Manual Installation 23 | 24 | 1. First we will need to make sure that the correct version of doom is in the correct directory. From the root of the SSS project, go into the the `doom` demo folder: 25 | 26 | ```bash 27 | cd demos/doom 28 | ``` 29 | 30 | 2. Clone the `chocolate-doom-sss` repo from GitHub into the current directory using `git` and then go its root folder: 31 | 32 | ```bash 33 | git clone git@github.com:christopolise/chocolate-doom-sss.git && cd chocolate-doom-sss 34 | ``` 35 | 36 | 3. Now we will make sure that our system is capable of running doom. We do this by running `autogen.sh`. If you get any errors, it will most likely be due to not having a certain library installed. 37 | 38 | ```bash 39 | ./autogen.sh 40 | ``` 41 | 42 | 4. Once you get `autogen` to finish correctly, then we make the binary of the project by: 43 | 44 | ```bash 45 | ./configure && make 46 | ``` 47 | 48 | 5. Once the `make` process has finished, you will be able to go to move the desired executable out of the `src` and into the `doom` demo folder: 49 | 50 | ```bash 51 | mv src/chocolate-doom .. 52 | ``` 53 | 54 | 6. After having done this, you are good to remove the `chocolate-doom-sss` folder as the `doom` demo only requires the executable. 55 | 56 | ```bash 57 | cd .. 58 | rm -rf chocolate-doom-sss 59 | ``` 60 | 61 | **NOTE: The version of doom you have is static. To update `chocolate-doom-sss`, you will need to run `setup.sh` or perform the manual installation again.** -------------------------------------------------------------------------------- /demos/doom/assets/full.wad: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/demos/doom/assets/full.wad -------------------------------------------------------------------------------- /demos/doom/assets/fuller.wad: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/demos/doom/assets/fuller.wad -------------------------------------------------------------------------------- /demos/doom/assets/miniwad.wad: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/demos/doom/assets/miniwad.wad -------------------------------------------------------------------------------- /demos/doom/assets/subvert.wad: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/demos/doom/assets/subvert.wad -------------------------------------------------------------------------------- /demos/doom/main.py: -------------------------------------------------------------------------------- 1 | from json import loads 2 | from math import sqrt 3 | from os import environ 4 | from os.path import exists 5 | from subprocess import DEVNULL, Popen 6 | 7 | import cv2 8 | import numpy as np 9 | from loguru import logger 10 | from sysv_ipc import ExistentialError, SharedMemory 11 | 12 | from demos.utils import get_all_from_queue 13 | 14 | DOOM_VIDEO_ID = 666 15 | DOOM_INPUT_ID = 667 16 | DOOM_OUTPUT_ID = 668 17 | 18 | SCREENWIDTH = 320 19 | SCREENHEIGHT = 200 20 | NUM_COLS = 48 21 | 22 | 23 | class Doom: 24 | """This is an interactive demo that adapts the Doom game for the SSS by capturing the game's video output""" 25 | 26 | demo_time = None # None for a game 27 | 28 | # User input is passed through input_queue 29 | # Game output is passed through output_queue 30 | # Screen updates are done through the screen object 31 | def __init__(self, input_queue, output_queue, screen): 32 | """ 33 | Constructor 34 | 35 | Args: 36 | input_queue (Queue): The input queue 37 | output_queue (Queue): The output queue 38 | screen (Screen): The screen to draw on 39 | 40 | Raises: 41 | Exception: If the game is not found 42 | """ 43 | # Provide the framerate in frames/seconds and the amount of time of the demo in seconds 44 | self.frame_rate = 50 45 | 46 | self.input_queue = input_queue 47 | self.output_queue = output_queue 48 | self.screen = screen 49 | 50 | self.game_installed = exists("demos/doom/chocolate-doom") 51 | 52 | self.shared_mem_init = False 53 | self.choco_doom = 0 54 | self.curmin = 0 55 | self.curmax = 0 56 | 57 | # Initialize game and allocate shared memory 58 | if self.game_installed: 59 | environ["SDL_VIDEODRIVER"] = "dummy" 60 | self.choco_doom = Popen( 61 | [ 62 | "./demos/doom/chocolate-doom", 63 | "-iwad", 64 | "assets/fuller.wad", 65 | "-file", 66 | "assets/subvert.wad", 67 | ], 68 | stdout=DEVNULL, 69 | stderr=DEVNULL, 70 | ) 71 | 72 | # init a connection to shared memory locations here 73 | try: 74 | self.shm = SharedMemory(DOOM_VIDEO_ID, 0, 0) 75 | self.shm.attach(0, 0) 76 | 77 | self.shm_input = SharedMemory(DOOM_INPUT_ID, 0, 0) 78 | self.shm_input.attach(0, 0) 79 | 80 | self.shm_output = SharedMemory(DOOM_OUTPUT_ID, 0, 0) 81 | self.shm_output.attach(0, 0) 82 | 83 | self.shared_mem_init = True 84 | except (ExistentialError, Exception) as e: 85 | logger.error("Could not establish connection with shared memory") 86 | logger.error(e) 87 | exit(0) 88 | 89 | # Color map for the SSS 90 | self.num_to_pixel = { 91 | 0: 0x0, 92 | 1: 0x0, 93 | 2: 0x0, 94 | 3: 0x0, 95 | 4: 0x0, 96 | 5: 0x2, 97 | 6: 0x2, 98 | 7: 0xA, 99 | 8: 0xA, 100 | 9: 0xE, 101 | 10: 0xE, 102 | 11: 0xF, 103 | 12: 0xF, 104 | } 105 | 106 | self.screen_max = 0 107 | self.screen_min = 0 108 | 109 | # Init numpy array for screen 110 | self.arr = [] 111 | for i in range(NUM_COLS): 112 | self.arr.append([]) 113 | for j in range(NUM_COLS): 114 | self.arr[i].append(0) 115 | 116 | def run(self): 117 | """Run the game""" 118 | # In case memory cannot be initialized correctly or game is not found 119 | if not self.game_installed or not self.shared_mem_init: 120 | self.screen.draw_text( 121 | self.screen.x_width // 2 - 10, 122 | self.screen.y_height // 2 - 4, 123 | "ERROR INITIALIZING DOOM", 124 | push=True, 125 | ) 126 | while True: 127 | yield 128 | 129 | # Create generator here 130 | while True: 131 | # Reading screen details from screen shared memory buffer 132 | buf = self.shm.read(SCREENWIDTH * SCREENHEIGHT) 133 | buf = np.frombuffer(bytearray(buf), dtype=np.uint8) 134 | buf = buf.reshape(SCREENHEIGHT, SCREENWIDTH) 135 | buf = buf[12:156, 16:304] 136 | buf = cv2.resize(buf, (48, 48)) 137 | 138 | # Load keypresses into shared memory for input to game 139 | outbuf = self.shm_output.read() 140 | outbuf = str(outbuf).split("|")[0][2:] 141 | outjson = loads(outbuf) 142 | self.output_queue.put(outjson) 143 | 144 | presses = "" 145 | 146 | for keypress in get_all_from_queue(self.input_queue): 147 | presses += keypress + "," 148 | presses = presses[:-1] 149 | 150 | self.shm_input.write(presses + "\0") 151 | 152 | self.screen_min = 0 153 | self.screen_max = 255 154 | 155 | for i in range(48): 156 | for j in range(48): 157 | pixel = int( 158 | ( 159 | (buf[i][j] - self.screen_min) 160 | / (self.screen_max - self.screen_min) 161 | ) 162 | * 12 163 | ) 164 | if pixel > 12: 165 | logger.debug(f"\nself.screen_min = {self.screen_min}") 166 | logger.debug(f"pixel = {pixel}") 167 | logger.debug(f"graySmall[{i}][{j}] = {buf[i][j]}") 168 | logger.debug(f"self.screen_max = {self.screen_max}") 169 | pixel = 12 170 | pixel = int(-1 * sqrt(12 * pixel) + 12) 171 | 172 | if self.arr[i][j] != self.num_to_pixel[pixel]: 173 | self.screen.draw_pixel( 174 | j, i, self.num_to_pixel[pixel], combine=False 175 | ) 176 | self.arr[i][j] = self.num_to_pixel[pixel] 177 | 178 | self.screen.push() 179 | 180 | yield 181 | 182 | def stop(self): 183 | """Stop the game""" 184 | # Close game, release memory to OS, and close processes attached 185 | logger.info("Releasing system resources...") 186 | if self.shared_mem_init: 187 | self.shm_input.write("QUIT_P") 188 | self.choco_doom.terminate() 189 | self.shm.detach() 190 | self.shm_input.detach() 191 | self.shm_output.detach() 192 | -------------------------------------------------------------------------------- /demos/doom/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | git clone git@github.com:christopolise/chocolate-doom-sss.git && cd chocolate-doom-sss 4 | 5 | GITSTEP=$(echo $?) 6 | if [ $GITSTEP -eq 0 ]; then 7 | echo "Git setup step passed"; 8 | else 9 | echo "Something went wrong in the setup. Please look at the output for more details. You can also try installing manually"; 10 | exit 11 | fi 12 | 13 | ./autogen.sh && ./configure && make 14 | 15 | BUILDSTEP=$(echo $?) 16 | 17 | if [ $BUILDSTEP -eq 0 ]; then 18 | echo "Build step passed"; 19 | else 20 | echo "Something went wrong in the setup. Please look at the output for more details. You can also try installing manually"; 21 | exit 22 | fi 23 | 24 | mv src/chocolate-doom .. && cd .. && rm -rf chocolate-doom-sss 25 | 26 | CLEANUPSTEP=$(echo $?) 27 | 28 | if [ $CLEANUPSTEP -eq 0 ]; then 29 | echo "Clean step passed"; 30 | else 31 | echo "Something went wrong in the setup. Please look at the output for more details. You can also try installing manually"; 32 | exit 33 | fi 34 | 35 | 36 | echo "Setup completed successfully" -------------------------------------------------------------------------------- /demos/game_of_life/main.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import copy 3 | import random 4 | 5 | 6 | class GameOfLife: 7 | """Game of Life simulation""" 8 | 9 | demo_time = 30 10 | 11 | def __init__(self, input_queue, output_queue, screen): 12 | """ 13 | Constructor 14 | 15 | Args: 16 | input_queue (Queue): The input queue 17 | output_queue (Queue): The output queue 18 | screen (Screen): The screen to draw on 19 | 20 | """ 21 | self.frame_rate = 10 22 | 23 | self.input_queue = input_queue 24 | self.output_queue = output_queue 25 | self.screen = screen 26 | 27 | self.new_board_wait_frames = self.frame_rate 28 | 29 | def _create_board(self, width, height, density=0.25): 30 | """ 31 | Create a new board 32 | 33 | Args: 34 | width (int): The width of the board 35 | height (int): The height of the board 36 | density (float): The density of the board 37 | 38 | Returns: 39 | list: The new board 40 | """ 41 | return [ 42 | [int(random.random() < density) for x in range(width)] 43 | for y in range(height) 44 | ] 45 | 46 | def _display_board(self, board): 47 | """ 48 | Display the board 49 | 50 | Args: 51 | board (list): The board to display 52 | 53 | """ 54 | for x in range(self.screen.x_width): 55 | for y in range(self.screen.y_height): 56 | self.screen.draw_pixel(x, y, 0xF if board[y][x] else 0x0) 57 | self.screen.push() 58 | 59 | def _update_board(self, board): 60 | """ 61 | Update the board 62 | 63 | Args: 64 | board (list): The board to update 65 | 66 | Returns: 67 | list: The updated board 68 | """ 69 | new_board = copy.deepcopy(board) 70 | for y in range(len(board)): 71 | for x in range(len(board[y])): 72 | alive = self._check_alive(x, y, board) 73 | new_board[y][x] = alive 74 | 75 | return new_board 76 | 77 | def _check_alive(self, x, y, board): 78 | """ 79 | Check if a cell is alive 80 | 81 | Args: 82 | x (int): The x position 83 | y (int): The y position 84 | board (list): The board 85 | 86 | Returns: 87 | int: 1 if alive, 0 if dead 88 | """ 89 | alive = board[y][x] 90 | board_width = len(board[0]) 91 | board_height = len(board) 92 | neighbors_alive = 0 93 | 94 | if x > 0: 95 | neighbors_alive += board[y][x - 1] 96 | if (x + 1) < board_width: 97 | neighbors_alive += board[y][x + 1] 98 | if y > 0: 99 | neighbors_alive += board[y - 1][x] 100 | if (y + 1) < board_height: 101 | neighbors_alive += board[y + 1][x] 102 | 103 | if x > 0 and y > 0: 104 | neighbors_alive += board[y - 1][x - 1] 105 | if x > 0 and (y + 1) < board_height: 106 | neighbors_alive += board[y + 1][x - 1] 107 | if (x + 1) < board_width and y > 0: 108 | neighbors_alive += board[y - 1][x + 1] 109 | if (x + 1) < board_width and (y + 1) < board_height: 110 | neighbors_alive += board[y + 1][x + 1] 111 | 112 | if alive: 113 | if neighbors_alive < 2 or neighbors_alive > 3: 114 | return 0 115 | else: 116 | return 1 117 | else: 118 | if neighbors_alive == 3: 119 | return 1 120 | else: 121 | return 0 122 | 123 | def run(self): 124 | """Run the simulation""" 125 | old_boards = collections.deque(maxlen=5) 126 | 127 | while True: 128 | # Set up the initial state 129 | self.screen.clear() 130 | old_boards.clear() 131 | board = self._create_board(self.screen.x_width, self.screen.y_height) 132 | 133 | # Display the board 134 | self._display_board(board) 135 | yield 136 | 137 | # Update the board for each tick as long as the board is not be repeated 138 | while board not in old_boards: 139 | old_boards.append(board) 140 | board = self._update_board(board) 141 | 142 | self._display_board(board) 143 | yield 144 | 145 | # Wait a little longer to show the old board 146 | for _ in range(self.new_board_wait_frames): 147 | yield 148 | 149 | def stop(self): 150 | """Stop the simulation""" 151 | pass 152 | -------------------------------------------------------------------------------- /demos/identify/main.py: -------------------------------------------------------------------------------- 1 | class Identify: 2 | """This is a boilerplate class for creating new demos/games for the SSS platform. It needs to include definitions for the following functions: init, run, stop. 3 | The init function needs to at least have the things shown below. Frame rate is in frames per second and demo time is in seconds. Demo time should be None if it is a game. 4 | """ 5 | 6 | demo_time = None # Number of seconds or None if its game 7 | 8 | # User input is passed through input_queue 9 | # Game output is passed through output_queue 10 | # Screen updates are done through the screen object 11 | def __init__(self, input_queue, output_queue, screen): 12 | """ 13 | Constructor 14 | 15 | Args: 16 | input_queue (Queue): The input queue 17 | output_queue (Queue): The output queue 18 | screen (Screen): The screen to draw on 19 | 20 | """ 21 | # Provide the framerate in frames/seconds and the amount of time of the demo in seconds 22 | self.frame_rate = 10 23 | 24 | self.input_queue = input_queue 25 | self.output_queue = output_queue 26 | self.screen = screen 27 | # init demo/game specific variables here 28 | 29 | def run(self): 30 | """The run function yields a generator. This generator will be called a specified frame rate, this controls what is being pushed to the screen.""" 31 | # Create generator here 32 | while True: 33 | for num_height in range(4): 34 | for num_width in range(3): 35 | self.screen.draw_text( 36 | num_width * 16, 37 | num_height * 12, 38 | str(num_height * 3 + num_width), 39 | push=True, 40 | ) 41 | yield 42 | 43 | def stop(self): 44 | """The stop function is called when the demo/game is being exited by the upper SSS software. It should reset the state for the game""" 45 | pass 46 | -------------------------------------------------------------------------------- /demos/letters/main.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | 4 | class Letters: 5 | """This is demo draws random symbols on the screen. Kind of fun to watch""" 6 | 7 | demo_time = 30 8 | 9 | # User input is passed through input_queue 10 | # Game output is passed through output_queue 11 | # Screen updates are done through the screen object 12 | def __init__(self, input_queue, output_queue, screen): 13 | """ 14 | Constructor 15 | 16 | Args: 17 | input_queue (Queue): The input queue 18 | output_queue (Queue): The output queue 19 | screen (Screen): The screen to draw on 20 | 21 | """ 22 | # Provide the framerate in frames/seconds and the amount of time of the demo in seconds 23 | self.frame_rate = 20 24 | 25 | self.input_queue = input_queue 26 | self.output_queue = output_queue 27 | self.screen = screen 28 | # init demo/game specific variables here 29 | 30 | def run(self): 31 | """Runs the simulation loop""" 32 | while True: 33 | for _ in range(4): 34 | self.screen.draw_text( 35 | random.randint(0, self.screen.x_width - 1), 36 | random.randint(0, self.screen.y_height - 2), 37 | chr(random.randint(33, 126)), 38 | ) 39 | for _ in range(4): 40 | self.screen.draw_text( 41 | random.randint(0, self.screen.x_width - 1), 42 | random.randint(0, self.screen.y_height - 2), 43 | " ", 44 | ) 45 | self.screen.push() 46 | yield 47 | 48 | def stop(self): 49 | """Reset the state of the demo if needed, else leave blank""" 50 | pass 51 | -------------------------------------------------------------------------------- /demos/rain/main.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from random import randint 3 | 4 | import numpy as np 5 | from perlin_noise import PerlinNoise 6 | 7 | from display.segment_display import SegmentDisplay 8 | 9 | 10 | @dataclass 11 | class RainDrop: 12 | x: float 13 | y: float 14 | speed: float 15 | 16 | 17 | class Rain: 18 | """ 19 | Simulates rain falling. Rain drops can be different lengths and 20 | different speeds. Wind blows the rain in random patterns. 21 | """ 22 | 23 | demo_time = 30 24 | 25 | def __init__(self, input_queue, output_queue, screen): 26 | """ 27 | Constructor 28 | 29 | Args: 30 | input_queue (Queue): Queue for receiving messages 31 | output_queue (Queue): Queue for sending messages 32 | screen (pygame.Surface): Surface to draw on 33 | """ 34 | self.frame_rate = 10 35 | 36 | self.input_queue = input_queue 37 | self.output_queue = output_queue 38 | self.screen = screen 39 | 40 | self.display = SegmentDisplay(self.screen) 41 | 42 | self.density = 15 43 | self.rain_length = 2 44 | 45 | def run(self): 46 | """Run the rain demo""" 47 | # Set up rain 48 | noise = PerlinNoise() 49 | rain = [ 50 | RainDrop( 51 | randint(0, self.display.width), 52 | randint(0, self.display.height), 53 | randint(1, 3), 54 | ) 55 | for _ in range(20 * self.density) 56 | ] 57 | count = 0 58 | 59 | while True: 60 | rain_dir = 6 * noise(count * 0.02) 61 | 62 | for this_rain in rain: 63 | self.display.draw_line( 64 | int(this_rain.x), 65 | int(this_rain.y), 66 | int(this_rain.x + rain_dir), 67 | int(this_rain.y + self.rain_length), 68 | ) 69 | 70 | this_rain.y += this_rain.speed 71 | this_rain.x += rain_dir 72 | 73 | if this_rain.y > self.display.height: 74 | this_rain.y = 0 75 | 76 | if this_rain.x < 0: 77 | this_rain.x = self.display.width 78 | 79 | if this_rain.x > self.display.width: 80 | this_rain.x = 0 81 | 82 | # Remove any horizontal components 83 | self.display.x_buffer = np.zeros((self.display.width, self.display.height)) 84 | 85 | self.display.draw() 86 | yield 87 | self.display.undraw() 88 | 89 | count += 1 90 | 91 | def stop(self): 92 | """Stop the rain demo""" 93 | pass 94 | -------------------------------------------------------------------------------- /demos/sine/main.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import numpy as np 4 | 5 | from display.segment_display import SegmentDisplay 6 | 7 | 8 | class Sine: 9 | """ 10 | Displays an animated sine wave that is increasing in frequency. When the 11 | frequency gets to a certain point, it moves to a lower frequency. This 12 | pattern is repeated. 13 | """ 14 | 15 | demo_time = 30 16 | 17 | def __init__(self, input_queue, output_queue, screen): 18 | """ 19 | Constructor 20 | 21 | Args: 22 | input_queue (Queue): Queue to receive messages from the main thread 23 | output_queue (Queue): Queue to send messages to the main thread 24 | screen (Screen): Surface to draw on 25 | 26 | """ 27 | self.frame_rate = 10 28 | 29 | self.input_queue = input_queue 30 | self.output_queue = output_queue 31 | self.screen = screen 32 | 33 | self.display = SegmentDisplay(self.screen) 34 | 35 | self.density = 15 36 | self.rain_length = 2 37 | 38 | def run(self): 39 | """Main loop""" 40 | paramx = 0.09 41 | sin = 30 42 | increase_factor = 1 43 | 44 | while True: 45 | sinPoints = self.display.width 46 | psinx = 0 47 | psiny = 0 48 | 49 | if sin == 30: 50 | increase_factor = 1 51 | 52 | if sin == 100: 53 | increase_factor = -1 54 | 55 | sin += increase_factor 56 | paramx += increase_factor * sin * 0.000176 57 | 58 | for i in range(sinPoints): 59 | sinx = int(float(i) / sinPoints * self.display.width) 60 | siny = int( 61 | (1 + math.sin(math.pi * 2 * 3 / 4 + i * paramx * 0.75)) 62 | / 2 63 | * self.display.height 64 | ) 65 | 66 | self.display.draw_line( 67 | psinx, 68 | psiny, 69 | sinx, 70 | siny, 71 | ) 72 | psinx = sinx 73 | psiny = siny 74 | 75 | self.display.draw() 76 | yield 77 | self.display.undraw() 78 | 79 | def stop(self): 80 | """Stop the thread""" 81 | pass 82 | -------------------------------------------------------------------------------- /demos/snake/high_score.txt: -------------------------------------------------------------------------------- 1 | 69 -------------------------------------------------------------------------------- /demos/snake/snek_state.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | 3 | import numpy as np 4 | 5 | 6 | class snek_state: 7 | """A class to represent the state of the snake game""" 8 | 9 | def __init__(self, width, height): 10 | """ 11 | Constructor 12 | 13 | Args: 14 | width (int): The width of the game board 15 | height (int): The height of the game board 16 | """ 17 | self.width = width 18 | self.height = height 19 | self.data = np.zeros((int(height), int(width))) 20 | for i in range(3): 21 | self.data[i] = np.ones(width) 22 | self.snek_parts = [] 23 | self.food_locs = [] 24 | 25 | def add_snake_part2(self, cord): 26 | """ 27 | Adds a snake part to the snake 28 | 29 | Args: 30 | cord (tuple): The coordinates of the snake part 31 | 32 | """ 33 | self.snek_parts.append(cord) 34 | 35 | def del_snake_part2(self): 36 | """ 37 | Deletes the last snake part 38 | 39 | Returns: 40 | tuple: The coordinates of the deleted snake part 41 | """ 42 | return self.snek_parts.pop(0) 43 | 44 | def add_food2(self, cord): 45 | """ 46 | Adds a food to the game board 47 | 48 | Args: 49 | cord (tuple): The coordinates of the food 50 | 51 | """ 52 | self.food_locs = cord 53 | 54 | def add_snake_part(self, cord): 55 | """ 56 | Adds a snake part to the snake 57 | 58 | Args: 59 | cord (tuple): The coordinates of the snake part 60 | 61 | """ 62 | self.data[int(cord[1])][int(cord[0])] = 1 63 | 64 | def del_snake_part(self, cord): 65 | """ 66 | Deletes a snake part from the snake 67 | 68 | Args: 69 | cord (tuple): The coordinates of the snake part 70 | 71 | """ 72 | self.data[int(cord[1])][int(cord[0])] = 0 73 | 74 | def add_food(self, cord): 75 | """ 76 | Adds a food to the game board 77 | 78 | Args: 79 | cord (tuple): The coordinates of the food 80 | 81 | """ 82 | self.data[int(cord[1])][int(cord[0])] = 2 83 | 84 | def del_food(self, cord): 85 | """ 86 | Deletes a food from the game board 87 | 88 | Args: 89 | cord (tuple): The coordinates of the food 90 | 91 | """ 92 | self.data[int(cord[1])][int(cord[0])] = 1 93 | self.food_locs.append(cord) 94 | 95 | 96 | class snek_Node: 97 | """A class to represent a node in the A* search""" 98 | 99 | def __init__(self, x, y, cost, prev, snake_parts, actual_cost): 100 | """ 101 | Constructor 102 | 103 | Args: 104 | x (int): The x coordinate of the node 105 | y (int): The y coordinate of the node 106 | cost (int): The cost of the node 107 | prev (snek_Node): The previous node 108 | snake_parts (list): The snake parts 109 | actual_cost (int): The actual cost of the node 110 | 111 | """ 112 | self.loc = (x, y) 113 | self.cost = cost 114 | self.prev = prev 115 | self.current_snake = snake_parts 116 | self.actual_cost = actual_cost 117 | 118 | def __copy__(self, newLoc): 119 | """ 120 | Copies the node 121 | 122 | Args: 123 | newLoc (tuple): The new location of the node 124 | """ 125 | self.loc = deepcopy(newLoc) 126 | 127 | def __eq__(self, other): 128 | """ 129 | Equals operator 130 | 131 | Args: 132 | other (snek_Node): The other node to compare to 133 | 134 | Returns: 135 | bool: True if the nodes are equal, False otherwise 136 | """ 137 | return self.cost == other.cost 138 | 139 | def __ne__(self, other): 140 | """ 141 | Not equals operator 142 | 143 | Args: 144 | other (snek_Node): The other node to compare to 145 | 146 | Returns: 147 | bool: True if the nodes are not equal, False otherwise 148 | """ 149 | return self.cost != other.cost 150 | 151 | def __lt__(self, other): 152 | """ 153 | Less than operator 154 | 155 | Args: 156 | other (snek_Node): The other node to compare to 157 | 158 | Returns: 159 | bool: True if the node is less than the other node, False otherwise 160 | """ 161 | return self.cost < other.cost 162 | 163 | def __le__(self, other): 164 | """ 165 | Less than or equal to operator 166 | 167 | Args: 168 | other (snek_Node): The other node to compare to 169 | 170 | Returns: 171 | bool: True if the node is less than or equal to the other node, False otherwise 172 | """ 173 | return self.cost <= other.cost 174 | 175 | def __gt__(self, other): 176 | """ 177 | Greater than operator 178 | 179 | Args: 180 | other (snek_Node): The other node to compare to 181 | 182 | Returns: 183 | bool: True if the node is greater than the other node, False otherwise 184 | """ 185 | return self.cost > other.cost 186 | 187 | def __ge__(self, other): 188 | """ 189 | Greater than or equal to operator 190 | 191 | Args: 192 | other (snek_Node): The other node to compare to 193 | 194 | Returns: 195 | bool: True if the node is greater than or equal to the other node, False otherwise 196 | """ 197 | return self.cost >= other.cost 198 | -------------------------------------------------------------------------------- /demos/snake_ai/ai_high_score.txt: -------------------------------------------------------------------------------- 1 | 231 2 | -------------------------------------------------------------------------------- /demos/snake_ai/snek_state.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | 3 | import numpy as np 4 | 5 | 6 | class snek_state: 7 | """A class to represent the state of the snake game""" 8 | 9 | def __init__(self, width, height): 10 | """ 11 | Constructor 12 | 13 | Args: 14 | width (int): The width of the game board 15 | height (int): The height of the game board 16 | """ 17 | self.width = width 18 | self.height = height 19 | self.data = np.zeros((int(height), int(width))) 20 | for i in range(3): 21 | self.data[i] = np.ones(width) 22 | self.snek_parts = [] 23 | self.food_locs = [] 24 | 25 | def add_snake_part2(self, cord): 26 | """ 27 | Adds a snake part to the snake 28 | 29 | Args: 30 | cord (tuple): The coordinates of the snake part 31 | 32 | """ 33 | self.snek_parts.append(cord) 34 | 35 | def del_snake_part2(self): 36 | """ 37 | Deletes the last snake part 38 | 39 | Returns: 40 | tuple: The coordinates of the deleted snake part 41 | """ 42 | return self.snek_parts.pop(0) 43 | 44 | def add_food2(self, cord): 45 | """ 46 | Adds a food to the game board 47 | 48 | Args: 49 | cord (tuple): The coordinates of the food 50 | 51 | """ 52 | self.food_locs = cord 53 | 54 | def add_snake_part(self, cord): 55 | """ 56 | Adds a snake part to the snake 57 | 58 | Args: 59 | cord (tuple): The coordinates of the snake part 60 | 61 | """ 62 | self.data[int(cord[1])][int(cord[0])] = 1 63 | 64 | def del_snake_part(self, cord): 65 | """ 66 | Deletes a snake part from the snake 67 | 68 | Args: 69 | cord (tuple): The coordinates of the snake part 70 | 71 | """ 72 | self.data[int(cord[1])][int(cord[0])] = 0 73 | 74 | def add_food(self, cord): 75 | """ 76 | Adds a food to the game board 77 | 78 | Args: 79 | cord (tuple): The coordinates of the food 80 | 81 | """ 82 | self.data[int(cord[1])][int(cord[0])] = 2 83 | 84 | def del_food(self, cord): 85 | """ 86 | Deletes a food from the game board 87 | 88 | Args: 89 | cord (tuple): The coordinates of the food 90 | 91 | """ 92 | self.data[int(cord[1])][int(cord[0])] = 1 93 | self.food_locs.append(cord) 94 | 95 | 96 | class snek_Node: 97 | """A class to represent a node in the A* search""" 98 | 99 | def __init__(self, x, y, cost, prev, snake_parts, actual_cost): 100 | """ 101 | Constructor 102 | 103 | Args: 104 | x (int): The x coordinate of the node 105 | y (int): The y coordinate of the node 106 | cost (int): The cost of the node 107 | prev (snek_Node): The previous node 108 | snake_parts (list): The snake parts 109 | actual_cost (int): The actual cost of the node 110 | 111 | """ 112 | self.loc = (x, y) 113 | self.cost = cost 114 | self.prev = prev 115 | self.current_snake = snake_parts 116 | self.actual_cost = actual_cost 117 | 118 | def __copy__(self, newLoc): 119 | """ 120 | Copies the node 121 | 122 | Args: 123 | newLoc (tuple): The new location of the node 124 | """ 125 | self.loc = deepcopy(newLoc) 126 | 127 | def __eq__(self, other): 128 | """ 129 | Equals operator 130 | 131 | Args: 132 | other (snek_Node): The other node to compare to 133 | 134 | Returns: 135 | bool: True if the nodes are equal, False otherwise 136 | """ 137 | return self.cost == other.cost 138 | 139 | def __ne__(self, other): 140 | """ 141 | Not equals operator 142 | 143 | Args: 144 | other (snek_Node): The other node to compare to 145 | 146 | Returns: 147 | bool: True if the nodes are not equal, False otherwise 148 | """ 149 | return self.cost != other.cost 150 | 151 | def __lt__(self, other): 152 | """ 153 | Less than operator 154 | 155 | Args: 156 | other (snek_Node): The other node to compare to 157 | 158 | Returns: 159 | bool: True if the node is less than the other node, False otherwise 160 | """ 161 | return self.cost < other.cost 162 | 163 | def __le__(self, other): 164 | """ 165 | Less than or equal to operator 166 | 167 | Args: 168 | other (snek_Node): The other node to compare to 169 | 170 | Returns: 171 | bool: True if the node is less than or equal to the other node, False otherwise 172 | """ 173 | return self.cost <= other.cost 174 | 175 | def __gt__(self, other): 176 | """ 177 | Greater than operator 178 | 179 | Args: 180 | other (snek_Node): The other node to compare to 181 | 182 | Returns: 183 | bool: True if the node is greater than the other node, False otherwise 184 | """ 185 | return self.cost > other.cost 186 | 187 | def __ge__(self, other): 188 | """ 189 | Greater than or equal to operator 190 | 191 | Args: 192 | other (snek_Node): The other node to compare to 193 | 194 | Returns: 195 | bool: True if the node is greater than or equal to the other node, False otherwise 196 | """ 197 | return self.cost >= other.cost 198 | -------------------------------------------------------------------------------- /demos/sound_visualizer/bensound-evolution.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/demos/sound_visualizer/bensound-evolution.npy -------------------------------------------------------------------------------- /demos/sound_visualizer/bensound-memories.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/demos/sound_visualizer/bensound-memories.npy -------------------------------------------------------------------------------- /demos/sound_visualizer/bensound-pianomoment.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/demos/sound_visualizer/bensound-pianomoment.npy -------------------------------------------------------------------------------- /demos/sound_visualizer/main.py: -------------------------------------------------------------------------------- 1 | # from display import utils 2 | import random 3 | 4 | import numpy as np 5 | 6 | 7 | def sound_visualizer(display, queue, sound_frames, fps): 8 | """ 9 | Display a sound visualizer on the display. 10 | 11 | Args: 12 | display (Display): Display object to draw on 13 | queue (Queue): Queue to receive messages from the main thread 14 | sound_frames (List[List[int]]): List of frames to display 15 | fps (int): Frames per second to display 16 | """ 17 | tick = utils.frameRate(fps) 18 | bottom = display.y_height - 1 19 | # Display first frame 20 | for x in range(display.x_width): 21 | display.draw_shape_line(x, bottom, x, bottom - sound_frames[0][x], 15) 22 | display.push() 23 | next(tick) 24 | # Display the rest of ticks 25 | input = "" 26 | for frame_num in range(1, len(sound_frames)): 27 | if not queue.empty(): 28 | input = queue.get(block=False) 29 | if input == b"q": 30 | display.clear() 31 | return 32 | 33 | for x in range(display.x_width): 34 | if sound_frames[frame_num][x] > sound_frames[frame_num - 1][x]: 35 | display.draw_shape_line( 36 | x, 37 | bottom - sound_frames[frame_num - 1][x], 38 | x, 39 | bottom - sound_frames[frame_num][x], 40 | 15, 41 | ) 42 | else: 43 | display.draw_shape_line( 44 | x, 45 | bottom - sound_frames[frame_num - 1][x], 46 | x, 47 | bottom - sound_frames[frame_num][x], 48 | 0, 49 | ) 50 | display.push() 51 | next(tick) 52 | 53 | 54 | def sound_visualizer_run(display, queue, mqtt_client): 55 | """Run the sound visualizer demo.""" 56 | input = "" 57 | while True: 58 | if not queue.empty(): 59 | input = queue.get(block=False) 60 | if input == b"q": 61 | display.clear() 62 | return 63 | break 64 | with open( 65 | "/home/pi/sss/demos/sound_visualizer/bensound-" + input.decode() + ".npy", "rb" 66 | ) as f: 67 | frames = np.load(f) 68 | sound_visualizer( 69 | display, 70 | queue, 71 | frames, 72 | 30, 73 | ) 74 | 75 | 76 | class SoundVisualizer: 77 | """This demo showcases a sound visualization.""" 78 | 79 | demo_time = None 80 | 81 | # User input is passed through input_queue 82 | # Game output is passed through output_queue 83 | # Screen updates are done through the screen object 84 | def __init__(self, input_queue, output_queue, screen): 85 | """ 86 | Constructor 87 | 88 | Args: 89 | input_queue (Queue): Queue to receive messages from the main thread 90 | output_queue (Queue): Queue to send messages to the main thread 91 | screen (Screen): Surface to draw on 92 | """ 93 | # Provide the framerate in frames/seconds and the amount of time of the demo in seconds 94 | self.frame_rate = 10 95 | 96 | self.input_queue = input_queue 97 | self.output_queue = output_queue 98 | self.screen = screen 99 | # init demo/game specific variables here 100 | 101 | def run(self): 102 | """Main loop""" 103 | while True: 104 | yield 105 | 106 | def stop(self): 107 | """Reset the state of the demo if needed, else leave blank""" 108 | pass 109 | 110 | 111 | # def sound_visualizer_run(display, queue): 112 | # sound_visualizer( 113 | # display, 114 | # queue, 115 | # [ 116 | # [random.randint(0, display.y_height - 1) for x in range(display.x_width)] 117 | # for frames in range(1000) 118 | # ], 119 | # 10, 120 | # ) 121 | -------------------------------------------------------------------------------- /demos/sound_visualizer/song_processing.py: -------------------------------------------------------------------------------- 1 | # Performed offline on a different computer 2 | 3 | import librosa 4 | import numpy as np 5 | import scipy.signal 6 | 7 | # Tunable parameters 8 | song_name = "bensound-evolution" 9 | hop_length_secs = 1 / 10 10 | bands = 48 # How many frequency bands? 11 | 12 | # filename = librosa.ex("~/Downloads/bensound-evolution") 13 | y, sr = librosa.load("~/Downloads/" + song_name + ".mp3", sr=22050) 14 | sound_length = y.shape[0] / sr 15 | print(f"{sound_length = }") 16 | hop_length_samples = int(hop_length_secs * sr) 17 | print(f"{hop_length_secs = }") 18 | print(f"{hop_length_samples = }") 19 | 20 | stft = np.abs(librosa.stft(y, n_fft=1024, hop_length=hop_length_samples)) 21 | num_bins, num_samples = stft.shape 22 | 23 | # This should be approximately `sound_length` now 24 | print(f"{num_samples * hop_length_secs = }") 25 | 26 | # Resample to the desired number of frequency bins 27 | stft2 = np.abs(scipy.signal.resample(stft, bands, axis=0)) 28 | stft2 = stft2 / np.max(stft2) # Normalize to 0..1 29 | stft2 = stft2 * 36 30 | stft2_int = stft2.astype(int).T 31 | print(len(stft2_int)) 32 | with open(song_name + ".npy", "wb") as f: 33 | np.save(f, stft2_int) 34 | -------------------------------------------------------------------------------- /demos/spiral/main.py: -------------------------------------------------------------------------------- 1 | from loguru import logger 2 | 3 | 4 | class Spiral: 5 | """This demo that just spirals around""" 6 | 7 | demo_time = None 8 | 9 | # User input is passed through input_queue 10 | # Game output is passed through output_queue 11 | # Screen updates are done through the screen object 12 | def __init__(self, input_queue, output_queue, screen): 13 | """ 14 | Constructor 15 | 16 | Args: 17 | input_queue (Queue): Queue for user input 18 | output_queue (Queue): Queue for game output 19 | screen (Screen): Screen object 20 | """ 21 | # Provide the framerate in frames/seconds and the amount of time of the demo in seconds 22 | self.frame_rate = 200 23 | 24 | self.input_queue = input_queue 25 | self.output_queue = output_queue 26 | self.screen = screen 27 | # init demo/game specific variables here 28 | 29 | def run(self): 30 | """Main loop for the demo""" 31 | width = self.screen.x_width 32 | height = self.screen.y_height 33 | across = width 34 | down = height 35 | x_in = 0 36 | y_in = 0 37 | # Create generator here 38 | while True: 39 | for x in range(x_in, across): 40 | self.screen.draw_pixel(x, y_in, 0xF) 41 | # if x > x_in: 42 | # screen.draw_pixel(x - 1, y_in, 0x0) 43 | self.screen.push() 44 | if x < across - 1: 45 | yield 46 | 47 | for y in range(y_in, down): 48 | self.screen.draw_pixel(x, y, 0xF) 49 | # if y > y_in: 50 | # screen.draw_pixel(x, y - 1, 0x0) 51 | self.screen.push() 52 | if y < down - 1: 53 | yield 54 | 55 | for x in range(across - 1, x_in - 1, -1): 56 | self.screen.draw_pixel(x, y, 0xF) 57 | # if x < across - 1: 58 | # screen.draw_pixel(x + 1, y, 0x0) 59 | self.screen.push() 60 | if x > 0: 61 | yield 62 | 63 | # if down - 1 == y_in + 1: 64 | # break 65 | 66 | for y in range(down - 1, y_in, -1): 67 | self.screen.draw_pixel(x, y, 0xF) 68 | # if y < down - 1: 69 | # screen.draw_pixel(x, y + 1, 0x0) 70 | self.screen.push() 71 | # if y > 0: 72 | yield 73 | # if y == y_in + 1: 74 | # screen.draw_pixel(x, y, 0x0) 75 | 76 | x_in += 1 77 | y_in += 1 78 | across -= 1 79 | down -= 1 80 | 81 | if across == width / 2: 82 | self.screen.clear() 83 | x_in = 0 84 | y_in = 0 85 | across = width 86 | down = height 87 | continue 88 | 89 | def stop(self): 90 | """Reset the state of the demo if needed, else leave blank""" 91 | pass 92 | -------------------------------------------------------------------------------- /demos/sweep/main.py: -------------------------------------------------------------------------------- 1 | class Sweep: 2 | """This demo sweeps back and forth across the screen""" 3 | 4 | demo_time = None 5 | 6 | # User input is passed through input_queue 7 | # Game output is passed through output_queue 8 | # Screen updates are done through the screen object 9 | def __init__(self, input_queue, output_queue, screen): 10 | """ 11 | Constructor 12 | 13 | Args: 14 | input_queue (Queue): Queue for user input 15 | output_queue (Queue): Queue for game output 16 | screen (Screen): Screen object 17 | """ 18 | # Provide the framerate in frames/seconds and the amount of time of the demo in seconds 19 | self.frame_rate = 50 20 | 21 | self.input_queue = input_queue 22 | self.output_queue = output_queue 23 | self.screen = screen 24 | # init demo/game specific variables here 25 | 26 | def run(self): 27 | """Run the demo""" 28 | # Create generator here 29 | while True: 30 | for column in range(0, self.screen.x_width): 31 | self.draw_vline_loc(column, 0xF) 32 | yield 33 | 34 | for column in range(self.screen.x_width - 1, -1, -1): 35 | self.draw_vline_loc(column, 0x0) 36 | yield 37 | 38 | def stop(self): 39 | """Reset the state of the demo if needed, else leave blank""" 40 | pass 41 | 42 | def draw_vline_loc(self, x, val): 43 | """ 44 | Draw a vertical line at location x with value val 45 | 46 | Args: 47 | x (int): x location of the line 48 | val (int): value of the line 49 | """ 50 | for pix in range(0, self.screen.y_height): 51 | self.screen.draw_pixel(x, pix, val) 52 | self.screen.push() 53 | -------------------------------------------------------------------------------- /demos/template/main.py: -------------------------------------------------------------------------------- 1 | class Template: 2 | """ 3 | This is a boilerplate class for creating new demos/games for the SSS platform. It needs to include definitions for the following functions: init, run, stop. 4 | The init function needs to at least have the things shown below. Frame rate is in frames per second and demo time is in seconds. Demo time should be None if it is a game. 5 | The run function yields a generator. This generator will be called a specified frame rate, this controls what is being pushed to the screen. 6 | The stop function is called when the demo/game is being exited by the upper SSS software. It should reset the state for the game 7 | """ 8 | 9 | demo_time = None # Number of seconds or None if its game 10 | 11 | # User input is passed through input_queue 12 | # Game output is passed through output_queue 13 | # Screen updates are done through the screen object 14 | def __init__(self, input_queue, output_queue, screen): 15 | """ 16 | Constructor 17 | 18 | Args: 19 | input_queue (Queue): Queue for user input 20 | output_queue (Queue): Queue for game output 21 | screen (Screen): Screen object 22 | """ 23 | # Provide the framerate in frames/seconds and the amount of time of the demo in seconds 24 | self.frame_rate = 10 25 | 26 | self.input_queue = input_queue 27 | self.output_queue = output_queue 28 | self.screen = screen 29 | # init demo/game specific variables here 30 | 31 | def run(self): 32 | """Main loop for the demo""" 33 | # Create generator here 34 | while True: 35 | self.screen.draw_text( 36 | self.screen.x_width // 2 - 5, 37 | self.screen.y_height // 2 - 4, 38 | "HELLO THERE", 39 | push=True, 40 | ) 41 | yield 42 | 43 | def stop(self): 44 | """Reset the state of the demo if needed, else leave blank""" 45 | pass 46 | -------------------------------------------------------------------------------- /demos/under_construction/main.py: -------------------------------------------------------------------------------- 1 | PIXEL_FULL = 0xF 2 | PIXEL_BOTTOM = 0x1 3 | PIXEL_LEFT = 0x2 4 | PIXEL_TOP = 0x4 5 | PIXEL_RIGHT = 0x8 6 | 7 | 8 | class UnderConstruction: 9 | """This demo just displays an underconstruction sign""" 10 | 11 | demo_time = None 12 | 13 | # User input is passed through input_queue 14 | # Game output is passed through output_queue 15 | # Screen updates are done through the screen object 16 | def __init__(self, input_queue, output_queue, screen): 17 | """ 18 | Constructor 19 | 20 | Args: 21 | input_queue (Queue): Input queue 22 | output_queue (Queue): Output queue 23 | screen (Screen): Screen object 24 | 25 | """ 26 | # Provide the framerate in frames/seconds and the amount of time of the demo in seconds 27 | self.frame_rate = 10 28 | 29 | self.input_queue = input_queue 30 | self.output_queue = output_queue 31 | self.screen = screen 32 | # init demo/game specific variables here 33 | 34 | def run(self): 35 | """Main loop of the demo""" 36 | # Create generator here 37 | xmax = self.screen.x_width 38 | ymax = self.screen.y_height 39 | line = self.screen.draw_shape_line 40 | dot = self.screen.draw_pixel 41 | 42 | while True: 43 | line(5, ymax // 2 - 10, 6, ymax // 2 - 10, PIXEL_TOP) 44 | line(5, ymax // 2 - 12, 6, ymax // 2 - 12, PIXEL_TOP) 45 | line(5, ymax // 2 - 8, 6, ymax // 2 - 8, PIXEL_BOTTOM) 46 | line(4, ymax // 2 - 8, 4, ymax // 2 - 10, PIXEL_RIGHT) 47 | line(7, ymax // 2 - 8, 7, ymax // 2 - 10, PIXEL_LEFT) 48 | dot(7, ymax // 2 - 12, PIXEL_RIGHT + PIXEL_TOP) 49 | dot(4, ymax // 2 - 12, PIXEL_LEFT + PIXEL_TOP) 50 | dot(8, ymax // 2 - 11, PIXEL_BOTTOM + PIXEL_LEFT) 51 | dot(3, ymax // 2 - 11, PIXEL_BOTTOM + PIXEL_RIGHT) 52 | dot(4, ymax // 2 - 6, PIXEL_TOP + PIXEL_LEFT) 53 | dot(3, ymax // 2 - 7, PIXEL_TOP + PIXEL_LEFT) 54 | dot(7, ymax // 2 - 6, PIXEL_TOP + PIXEL_RIGHT) 55 | dot(8, ymax // 2 - 7, PIXEL_TOP + PIXEL_RIGHT) 56 | line(2, ymax // 2 - 8, 2, ymax // 2 - 10, PIXEL_RIGHT) 57 | line(9, ymax // 2 - 8, 9, ymax // 2 - 10, PIXEL_LEFT) 58 | line(4, ymax // 2 - 5, 4, ymax // 2 + 5, PIXEL_RIGHT) 59 | line(7, ymax // 2 - 5, 7, ymax // 2 + 5, PIXEL_LEFT) 60 | dot(4, ymax // 2 + 6, PIXEL_TOP + PIXEL_LEFT) 61 | dot(3, ymax // 2 + 7, PIXEL_TOP + PIXEL_LEFT) 62 | dot(7, ymax // 2 + 6, PIXEL_TOP + PIXEL_RIGHT) 63 | dot(8, ymax // 2 + 7, PIXEL_TOP + PIXEL_RIGHT) 64 | line(2, ymax // 2 + 8, 2, ymax // 2 + 11, PIXEL_RIGHT) 65 | line(9, ymax // 2 + 8, 9, ymax // 2 + 11, PIXEL_LEFT) 66 | dot(3, ymax // 2 + 12, PIXEL_LEFT + PIXEL_BOTTOM) 67 | dot(8, ymax // 2 + 12, PIXEL_RIGHT + PIXEL_BOTTOM) 68 | dot(4, ymax // 2 + 13, PIXEL_FULL - PIXEL_TOP) 69 | dot(7, ymax // 2 + 13, PIXEL_FULL - PIXEL_TOP) 70 | line(4, ymax // 2 + 8, 4, ymax // 2 + 11, PIXEL_RIGHT) 71 | line(7, ymax // 2 + 8, 7, ymax // 2 + 11, PIXEL_LEFT) 72 | line(5, ymax // 2 + 7, 6, ymax // 2 + 7, PIXEL_BOTTOM) 73 | 74 | self.screen.draw_text(16, 12, "UNDER") 75 | self.screen.draw_text(16, 16, "CONSTRUCTION") 76 | self.screen.push() 77 | yield 78 | 79 | def stop(self): 80 | """Reset the state of the demo if needed, else leave blank""" 81 | pass 82 | -------------------------------------------------------------------------------- /demos/utils.py: -------------------------------------------------------------------------------- 1 | def get_all_from_queue(queue): 2 | """ 3 | Helper function that gets all items from a given queue. 4 | 5 | Args: 6 | queue (Queue): The queue to get items from 7 | 8 | """ 9 | while not queue.empty(): 10 | yield queue.get() 11 | -------------------------------------------------------------------------------- /demos/video/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | 4 | import numpy as np 5 | from loguru import logger 6 | 7 | from demos.utils import get_all_from_queue 8 | 9 | 10 | class Video: 11 | """ 12 | This demo takes a pre-processed video and plays it on the sss. It randomly chooses between the available 13 | assets and allows the user to play, pause, go through the video frame by frame, and switch between videos 14 | """ 15 | 16 | demo_time = 120 17 | 18 | # User input is passed through input_queue 19 | # Game output is passed through output_queue 20 | # Screen updates are done through the screen object 21 | def __init__(self, input_queue, output_queue, screen): 22 | """ 23 | Constructor 24 | 25 | Args: 26 | input_queue (Queue): Queue for user input 27 | output_queue (Queue): Queue for game output 28 | screen (Screen): Screen object 29 | """ 30 | # Provide the framerate in frames/seconds and the amount of time of the demo in seconds 31 | self.frame_rate = 25 32 | 33 | self.input_queue = input_queue 34 | self.output_queue = output_queue 35 | self.screen = screen 36 | 37 | # init demo/game specific variables here 38 | self.path = "./demos/video/resources/pre-processed/" 39 | self.targets = os.listdir(self.path) 40 | self.address = random.randint(0, len(self.targets) - 1) 41 | self.target = self.targets[self.address] 42 | self.pause = False 43 | self.new_video = False 44 | self.next_frame = False 45 | self.previous_frame = np.zeros((self.screen.x_width, self.screen.y_height)) 46 | 47 | # Get the next video in the list 48 | def get_next_video(self): 49 | """Get the next video in the list""" 50 | if self.address < (len(self.targets) - 1): 51 | self.address += 1 52 | self.target = self.targets[self.address] 53 | else: 54 | self.address = 0 55 | self.target = self.targets[self.address] 56 | 57 | # Parse the user input 58 | def input_parsing(self, input_queue): 59 | """Parse the user input""" 60 | for input in input_queue: # allows the user to pause the video 61 | if input == "LEFT_P": 62 | self.pause = True 63 | if input == "UP_P": 64 | self.pause = True 65 | self.next_frame = True 66 | if input == "RIGHT_P": 67 | self.pause = False 68 | if input == "DOWN_P": 69 | self.new_video = True 70 | 71 | # Draws frame to screen 72 | def draw_frame(self, frame): 73 | """Draws frame to screen""" 74 | # Get frame of which pixels need to get updated 75 | diff_frame = np.not_equal(frame, self.previous_frame) 76 | 77 | for r in range(self.screen.x_width): 78 | for c in range(self.screen.y_height): 79 | # If pixel is different from last frame, update 80 | if diff_frame[r, c]: 81 | self.screen.draw_pixel(c, r, int(frame[r, c])) 82 | 83 | # Update previous frame 84 | self.previous_frame = frame.copy() 85 | 86 | # Push the frame to the screen 87 | self.screen.push() 88 | 89 | def run(self): 90 | """Run the demo""" 91 | # Create generator here 92 | while True: 93 | self.new_video = False 94 | self.pause = False 95 | self.screen.clear() 96 | self.screen.push() 97 | yield 98 | 99 | # Load the video 100 | loaded_video = np.load(self.path + self.target) 101 | loaded_video = loaded_video["arr_0"] 102 | 103 | # Iterate through the frames 104 | for frame in loaded_video: 105 | self.next_frame = False 106 | 107 | # Parse through the user input 108 | if not self.input_queue.empty(): 109 | while True: 110 | action = self.input_parsing( 111 | get_all_from_queue(self.input_queue) 112 | ) 113 | 114 | self.input_queue.queue.clear() 115 | 116 | # If the user wants to go to the next video, break out of the loop 117 | if (not self.pause) or self.next_frame or self.new_video: 118 | break 119 | yield 120 | 121 | # Skip to the next video 122 | if self.new_video: 123 | break 124 | 125 | # Draw frame to screen 126 | self.draw_frame(frame) 127 | yield 128 | 129 | # Go to next video 130 | self.get_next_video() 131 | 132 | def stop(self): 133 | """Reset the state of the demo if needed, else leave blank""" 134 | self.screen.clear() 135 | pass 136 | 137 | def get_input_buff(self): 138 | """Get all input off the queue""" 139 | return list(self.input_queue.queue) 140 | -------------------------------------------------------------------------------- /demos/video/resources/pre-processed/Avatar.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/demos/video/resources/pre-processed/Avatar.npz -------------------------------------------------------------------------------- /demos/video/resources/pre-processed/Bob_Ross.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/demos/video/resources/pre-processed/Bob_Ross.npz -------------------------------------------------------------------------------- /demos/video/resources/pre-processed/Maxwell.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/demos/video/resources/pre-processed/Maxwell.npz -------------------------------------------------------------------------------- /demos/video/resources/pre-processed/Rick.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/demos/video/resources/pre-processed/Rick.npz -------------------------------------------------------------------------------- /demos/video/resources/pre-processed/Stick_Bug.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/demos/video/resources/pre-processed/Stick_Bug.npz -------------------------------------------------------------------------------- /demos/video/resources/pre-processed/nyan.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/demos/video/resources/pre-processed/nyan.npz -------------------------------------------------------------------------------- /demos/video/resources/pre-processed/shrek.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/demos/video/resources/pre-processed/shrek.npz -------------------------------------------------------------------------------- /demos/video/resources/pre-processed/sully.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/demos/video/resources/pre-processed/sully.npz -------------------------------------------------------------------------------- /demos/video/video_processing.py: -------------------------------------------------------------------------------- 1 | import os 2 | from os.path import exists 3 | 4 | import cv2 5 | import numpy as np 6 | from loguru import logger 7 | 8 | # init demo/game specific variables here 9 | num_to_pixel = { 10 | 0: 0x0, 11 | 1: 0x0, 12 | 2: 0x0, 13 | 3: 0x0, 14 | 4: 0x0, 15 | 5: 0x2, 16 | 6: 0x2, 17 | 7: 0xA, 18 | 8: 0xA, 19 | 9: 0xE, 20 | 10: 0xE, 21 | 11: 0xF, 22 | 12: 0xF, 23 | } 24 | num_to_pixel_inverted = { 25 | 12: 0x0, 26 | 11: 0x0, 27 | 10: 0x0, 28 | 9: 0x0, 29 | 8: 0x0, 30 | 7: 0x2, 31 | 6: 0x2, 32 | 5: 0xA, 33 | 4: 0xA, 34 | 3: 0xE, 35 | 2: 0xE, 36 | 1: 0xF, 37 | 0: 0xF, 38 | } 39 | 40 | # Set screen max and min values to 0 41 | screen_min = 0 42 | screen_max = 0 43 | pause = False 44 | 45 | # Create directory for pre-processed videos 46 | path = "./demos/video/resources/videos/" 47 | path_processed = "./demos/video/resources/pre-processed/" 48 | try: 49 | os.mkdir(path) 50 | except: 51 | pass 52 | else: 53 | logger.info(f"Created directory {path}") 54 | 55 | # Get all videos in the directory 56 | targets = os.listdir(path) 57 | logger.info(f"Found {len(targets)} videos") 58 | 59 | 60 | # Normalize the values in the video 61 | def normalize(x): 62 | """ 63 | Normalize the values in the video 64 | 65 | Args: 66 | x (int): The value to normalize 67 | 68 | Returns: 69 | int: The normalized value 70 | """ 71 | return num_to_pixel[int((x - screen_min) / (screen_max / 12))] 72 | 73 | 74 | # Iterate through all videos 75 | for target in targets: 76 | # Open the video and get the total number of frames 77 | cap = cv2.VideoCapture(f"{path}{target}") 78 | total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) 79 | 80 | # Check if the video has already been processed 81 | if os.path.exists(f"{path_processed}{target[:-4]}.npz"): 82 | processing = False 83 | logger.info(f"{target} has already been processed") 84 | else: 85 | processing = True 86 | logger.info(f"processing {target}") 87 | 88 | # Iterate through all frames in the video 89 | video = np.array([]) 90 | while processing: 91 | (ret, frame) = cap.read() 92 | if not ret: 93 | # Reshape the video and save it 94 | video = video.reshape((total_frames, 48, 48)) 95 | video = video.astype(np.uint8) 96 | np.savez_compressed(f"{path_processed}{target[:-4]}.npz", video) 97 | logger.info(f"processed {target}") 98 | break 99 | 100 | # fmt: off 101 | grayFrame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # convert to grayscale 102 | graySmall = np.array(cv2.resize(grayFrame, [48, 48])) # resize to 48x48 103 | screen_max = max(graySmall.max(), 12) # get the max value in the frame 104 | screen_min = graySmall.min() # get the min value in the frame 105 | vector_func = np.vectorize(normalize) # vectorize the normalize function 106 | graySmall = vector_func(graySmall) # normalize the frame 107 | video = np.append(video, graySmall) # append the frame to the video 108 | # fmt: on 109 | 110 | # Release the video 111 | cap.release() 112 | -------------------------------------------------------------------------------- /display/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/display/__init__.py -------------------------------------------------------------------------------- /display/physical_screen.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import time 3 | 4 | from .display import Display 5 | from .seven_seg import SevenSegment 6 | 7 | 8 | class PhysicalScreen: 9 | """PhysicalScreen class to handle the physical screen""" 10 | 11 | def __init__(self, brightness=3): 12 | """ 13 | Constructor 14 | 15 | Args: 16 | brightness (int, optional): Brightness of the screen. Defaults to 3. 17 | 18 | """ 19 | self.brightness = brightness 20 | self.cs_num_lst = [ 21 | 11, 22 | 9, 23 | 14, 24 | 3, 25 | 7, 26 | 13, 27 | 10, 28 | 5, 29 | 6, 30 | 2, 31 | 12, 32 | 4, 33 | ] # channel select gpio pin numbers, array is in flattened row order from the top of the screen 34 | self.num_segs_across = 3 35 | self.num_segs_down = 4 36 | 37 | self._create_display() 38 | 39 | def _create_display(self): 40 | """Create the display""" 41 | panel_array = [ 42 | [ 43 | SevenSegment( 44 | num_digits=96, 45 | cs_num=self.cs_num_lst[i * self.num_segs_across + j], 46 | brightness=self.brightness, 47 | segment_orientation_array=[ 48 | [1, 2], 49 | [3, 4], 50 | [5, 6], 51 | [7, 8], 52 | [9, 10], 53 | [11, 12], 54 | ], 55 | ) 56 | for j in range(self.num_segs_across) 57 | ] 58 | for i in range(self.num_segs_down) 59 | ] 60 | 61 | self.display = Display( 62 | panel_array, 63 | 48, 64 | 48, 65 | ) 66 | 67 | def _close_display(self): 68 | """Close the display""" 69 | for row in range(len(self.display.board_objects)): 70 | for panel in range(len(self.display.board_objects[row])): 71 | self.display.board_objects[row][panel].close() 72 | 73 | def create_tick(self, frame_rate): 74 | """ 75 | Create a tick 76 | 77 | Args: 78 | frame_rate (int): Frame rate 79 | 80 | """ 81 | period = 1.0 / frame_rate 82 | nextTime = time.time() + period 83 | 84 | for i in itertools.count(): 85 | now = time.time() 86 | toSleep = nextTime - now 87 | 88 | if toSleep > 0: 89 | time.sleep(toSleep) 90 | nextTime += period 91 | else: 92 | nextTime = now + period 93 | 94 | yield i, nextTime 95 | 96 | def clear(self): 97 | """Clear the screen""" 98 | self.display.clear() 99 | 100 | def refresh(self): 101 | """Refresh the screen""" 102 | self._close_display() 103 | self._create_display() 104 | -------------------------------------------------------------------------------- /display/physical_screen_v2.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import time 3 | 4 | from .display import Display 5 | from .seven_seg_v2 import SevenSegment 6 | 7 | 8 | class PhysicalScreen: 9 | def __init__(self, brightness=3): 10 | self.brightness = brightness 11 | self.num_segs_across = 1 12 | self.num_segs_down = 1 13 | self._create_display() 14 | 15 | def _create_display(self): 16 | # need to have an array of ip addresses if more panels 17 | panel_array = [ 18 | [ 19 | SevenSegment(ip_address="172.0.0.3", brightness=self.brightness) 20 | for j in range(self.num_segs_across) 21 | ] 22 | for i in range(self.num_segs_down) 23 | ] 24 | 25 | self.display = Display( 26 | panel_array, 27 | self.num_segs_across * 16, 28 | self.num_segs_down * 6 * 2, 29 | ) 30 | 31 | def _close_display(self): 32 | for row in range(len(self.display.board_objects)): 33 | for panel in range(len(self.display.board_objects[row])): 34 | self.display.board_objects[row][panel].close() 35 | 36 | def create_tick(self, frame_rate): 37 | period = 1.0 / frame_rate 38 | nextTime = time.time() + period 39 | 40 | for i in itertools.count(): 41 | now = time.time() 42 | toSleep = nextTime - now 43 | 44 | if toSleep > 0: 45 | time.sleep(toSleep) 46 | nextTime += period 47 | else: 48 | nextTime = now + period 49 | 50 | yield i, nextTime 51 | 52 | def clear(self): 53 | self.display.clear() 54 | 55 | def refresh(self): 56 | self._close_display() 57 | self._create_display() 58 | -------------------------------------------------------------------------------- /display/segment_display.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | class SegmentDisplay: 5 | """ 6 | SegmentDisplay is a class that abstracts the S^3 screen into individually 7 | controllable segments. Instead of making each circle of a segment a pixel, 8 | this class lets you control each segment as a pixel. This means this 9 | display is 2 times wider (there are two horizontal segments) and 3 times 10 | higher (there are three vertical segments) than the Display object. 11 | """ 12 | 13 | def __init__(self, screen): 14 | """ 15 | Constructor 16 | 17 | Args: 18 | screen (Display): the screen object to draw on 19 | """ 20 | 21 | self.screen = screen 22 | 23 | self.screen_width = screen.x_width - 1 24 | self.screen_height = screen.y_height // 2 25 | 26 | self.width = 2 * self.screen_width 27 | self.height = 3 * self.screen_height 28 | 29 | self.x_buffer = np.zeros((self.width, self.height)) 30 | self.y_buffer = np.zeros((self.width, self.height)) 31 | 32 | def draw(self, push=True): 33 | """ 34 | Updates the screen with all of the lines drawn using draw_line. 35 | 36 | Args: 37 | push (bool): when true all the recent changes are pushed to the display 38 | """ 39 | for x in range(self.screen_width): 40 | for y in range(self.screen_height): 41 | state = 0 42 | 43 | if self.x_buffer[x * 2][y * 3]: 44 | state += 64 # 64 = TOP 45 | if self.x_buffer[x * 2][y * 3 + 1]: 46 | state += 1 # 1 = CENTER 47 | if self.x_buffer[x * 2][y * 3 + 2]: 48 | state += 8 # 8 = BOTTOM 49 | 50 | if self.y_buffer[x * 2 + 1][y * 3]: 51 | state += 32 # 32 = TR 52 | if self.y_buffer[x * 2 + 1][y * 3 + 1]: 53 | state += 16 # 16 = BR 54 | if self.y_buffer[x * 2][y * 3 + 1]: 55 | state += 4 # 4 = BL 56 | if self.y_buffer[x * 2][y * 3]: 57 | state += 2 # 2 = TL 58 | 59 | # Only draw the pixel if there is something to draw 60 | if state != 0: 61 | self.screen.draw_raw(x, y, state) 62 | 63 | if push: 64 | self.screen.push() 65 | 66 | def undraw(self): 67 | """ 68 | Clears all segments drawn both from the screen and from the underlying data structure. 69 | """ 70 | for x in range(self.screen_width): 71 | for y in range(self.screen_height): 72 | if self.x_buffer[x * 2][y * 3]: 73 | value = self.screen.get_raw(x, y) 74 | self.screen.draw_raw(x, y, value - 64) 75 | self.x_buffer[x * 2][y * 3] = False 76 | 77 | if self.x_buffer[x * 2][y * 3 + 1]: 78 | value = self.screen.get_raw(x, y) 79 | self.screen.draw_raw(x, y, value - 1) 80 | self.x_buffer[x * 2][y * 3 + 1] = False 81 | 82 | if self.x_buffer[x * 2][y * 3 + 2]: 83 | value = self.screen.get_raw(x, y) 84 | self.screen.draw_raw(x, y, value - 8) 85 | self.x_buffer[x * 2][y * 3 + 2] = False 86 | 87 | if self.y_buffer[x * 2 + 1][y * 3]: 88 | value = self.screen.get_raw(x, y) 89 | self.screen.draw_raw(x, y, value - 32) 90 | self.y_buffer[x * 2 + 1][y * 3] = False 91 | 92 | if self.y_buffer[x * 2 + 1][y * 3 + 1]: 93 | value = self.screen.get_raw(x, y) 94 | self.screen.draw_raw(x, y, value - 16) 95 | self.y_buffer[x * 2 + 1][y * 3 + 1] = False 96 | 97 | if self.y_buffer[x * 2][y * 3 + 1]: 98 | value = self.screen.get_raw(x, y) 99 | self.screen.draw_raw(x, y, value - 4) 100 | self.y_buffer[x * 2][y * 3 + 1] = False 101 | 102 | if self.y_buffer[x * 2][y * 3]: 103 | value = self.screen.get_raw(x, y) 104 | self.screen.draw_raw(x, y, value - 2) 105 | self.y_buffer[x * 2][y * 3] = False 106 | 107 | def draw_line(self, start_x, start_y, end_x, end_y): 108 | """ 109 | Draws a line from coordinate to another. To display the line, you must 110 | use the draw function. This function only updates the underlying data 111 | buffer. 112 | 113 | Args: 114 | start_x (int): the starting x point 115 | start_y (int): the starting y point 116 | end_x (int): the ending x point 117 | end_y (int): the ending y point 118 | """ 119 | start_x = self._constrain(start_x, 0, self.width - 1) 120 | start_y = self._constrain(start_y, 0, self.height - 1) 121 | 122 | end_x = self._constrain(end_x, 0, self.width - 1) 123 | end_y = self._constrain(end_y, 0, self.height - 1) 124 | 125 | if end_x < start_x: 126 | start_x, end_x = end_x, start_x 127 | start_y, end_y = end_y, start_y 128 | 129 | dx = end_x - start_x 130 | dy = end_y - start_y 131 | 132 | r = 0 133 | ny = 0 134 | pny = 0 135 | nny = 0 136 | 137 | p = dy / dx if dx != 0 else 0 138 | t = 0 139 | 140 | for i in range(dx + 1): 141 | r = int(round(t)) 142 | pny = ny 143 | ny = start_y + r 144 | 145 | if i > 0: # vertical lines connecting horizontal lines 146 | for j in range(abs(ny - pny)): 147 | if pny > ny: 148 | nny = pny - j - 1 149 | else: 150 | nny = pny + j 151 | 152 | self.y_buffer[start_x + i][nny] = 1 153 | 154 | if i != dx: 155 | self.x_buffer[start_x + i][ny] = 1 156 | t += p 157 | 158 | if dx == 0 and dy != 0: # in case of no vertical lines 159 | fs = 0 160 | fe = int(dy) 161 | 162 | if dy < 0: 163 | fs = fe 164 | fe = 0 165 | 166 | for i in range(fs, fe): 167 | self.y_buffer[start_x][start_y + i] = 1 168 | 169 | def _constrain(self, val, min_val, max_val): 170 | """A helper function that constrains a value between two values""" 171 | return min(max_val, max(min_val, val)) 172 | -------------------------------------------------------------------------------- /display/symbols.py: -------------------------------------------------------------------------------- 1 | # Adopted from https://github.com/dmadison/LED-Segment-ASCII and https://github.com/JennaSys/micropython-max7219/blob/master/seven_segment_ascii.py 2 | 3 | # 7 Segment bit order: DP-G-F-E-D-C-B-A 4 | char_map = { 5 | " ": 0b00000000, 6 | "!": 0b10000110, 7 | '"': 0b00100010, 8 | "#": 0b01111110, 9 | "$": 0b01101101, 10 | "%": 0b11010010, 11 | "&": 0b01000110, 12 | "'": 0b00100000, 13 | "(": 0b00101001, 14 | ")": 0b00001011, 15 | "*": 0b00100001, 16 | "+": 0b01110000, 17 | ",": 0b00010000, 18 | "-": 0b01000000, 19 | ".": 0b10000000, 20 | "/": 0b01010010, 21 | "0": 0b00111111, 22 | "1": 0b00000110, 23 | "2": 0b01011011, 24 | "3": 0b01001111, 25 | "4": 0b01100110, 26 | "5": 0b01101101, 27 | "6": 0b01111101, 28 | "7": 0b00000111, 29 | "8": 0b01111111, 30 | "9": 0b01101111, 31 | ":": 0b00001001, 32 | ";": 0b00001101, 33 | "<": 0b01100001, 34 | "=": 0b01001000, 35 | ">": 0b01000011, 36 | "?": 0b11010011, 37 | "@": 0b01011111, 38 | "A": 0b01110111, 39 | "B": 0b01111100, 40 | "C": 0b00111001, 41 | "D": 0b01011110, 42 | "E": 0b01111001, 43 | "F": 0b01110001, 44 | "G": 0b00111101, 45 | "H": 0b01110110, 46 | "I": 0b00110000, 47 | "J": 0b00011110, 48 | "K": 0b01110101, 49 | "L": 0b00111000, 50 | "M": 0b00010101, 51 | "N": 0b00110111, 52 | "O": 0b00111111, 53 | "P": 0b01110011, 54 | "Q": 0b01101011, 55 | "R": 0b00110011, 56 | "S": 0b01101101, 57 | "T": 0b01111000, 58 | "U": 0b00111110, 59 | "V": 0b00111110, 60 | "W": 0b00101010, 61 | "X": 0b01110110, 62 | "Y": 0b01101110, 63 | "Z": 0b01011011, 64 | "[": 0b00111001, 65 | "\\": 0b01100100, 66 | "]": 0b00001111, 67 | "^": 0b00100011, 68 | "_": 0b00001000, 69 | "`": 0b00000010, 70 | "a": 0b01011111, 71 | "b": 0b01111100, 72 | "c": 0b01011000, 73 | "d": 0b01011110, 74 | "e": 0b01111011, 75 | "f": 0b01110001, 76 | "g": 0b01101111, 77 | "h": 0b01110100, 78 | "i": 0b00010000, 79 | "j": 0b00001100, 80 | "k": 0b01110101, 81 | "l": 0b00110000, 82 | "m": 0b00010100, 83 | "n": 0b01010100, 84 | "u": 0b00011100, 85 | "v": 0b00011100, 86 | "w": 0b00010100, 87 | "x": 0b01110110, 88 | "y": 0b01101110, 89 | "z": 0b01011011, 90 | "{": 0b01000110, 91 | "|": 0b00110000, 92 | "}": 0b01110000, 93 | "~": 0b00000001, 94 | } 95 | 96 | 97 | def get_char(char): 98 | """ 99 | Get the 7-segment code for a character. 100 | 101 | Args: 102 | char (str): The character to get the code for. 103 | 104 | Returns: 105 | int: The 7-segment code for the character. 106 | """ 107 | return char_map.get(str(char), char_map.get("_")) 108 | 109 | 110 | def get_char2(char): 111 | """ 112 | Get the 7-segment code for a character. 113 | 114 | Args: 115 | char (str): The character to get the code for. 116 | 117 | Returns: 118 | int: The 7-segment code for the character. 119 | """ 120 | # 7 Segment bit order: DP-A-B-C-D-E-F-G 121 | bits = get_char(char) 122 | tmp = "{:08b}".format(bits) 123 | return int("".join(["0b", tmp[0], "".join(reversed(tmp[1:]))]), 2) 124 | -------------------------------------------------------------------------------- /display/virtual_screen.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | from pygame.locals import QUIT 3 | 4 | from .display import Display 5 | from .virtual_seven_seg import VirtualSevenSegment 6 | 7 | 8 | class VirtualScreen: 9 | """Virtual screen for the emulator.""" 10 | 11 | def __init__(self): 12 | """Constructor for the VirtualScreen class.""" 13 | pygame.init() 14 | 15 | self.window = pygame.display.set_mode((25 * 48 + 150, 30 * 24 + 30)) 16 | self.window.fill((0, 0, 0)) 17 | 18 | boards = [ 19 | [ 20 | VirtualSevenSegment(i * 16 * 25, j * 30 * 6, self.window) 21 | for i in range(3) 22 | ] 23 | for j in range(4) 24 | ] 25 | 26 | self.display = Display(boards, 16 * 3, 12 * 4) 27 | self.display.clear() 28 | 29 | def create_tick(self, frame_rate): 30 | """ 31 | Creates a tick generator for the virtual screen. 32 | 33 | Args: 34 | frame_rate (int): The frame rate of the virtual screen. 35 | """ 36 | clock = pygame.time.Clock() 37 | 38 | while True: 39 | pygame.display.flip() 40 | clock.tick(frame_rate) 41 | yield 42 | 43 | def clear(self): 44 | """Clears the virtual screen.""" 45 | self.display.clear() 46 | 47 | def refresh(self): 48 | """Refreshes the virtual screen.""" 49 | self.display.clear() 50 | -------------------------------------------------------------------------------- /display/virtual_seven_seg.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | 3 | 4 | class VirtualSevenSegment: 5 | """Virtual seven segment display.""" 6 | 7 | def __init__(self, start_x, start_y, display): 8 | """ 9 | Constructor 10 | 11 | Args: 12 | start_x (int): The starting x position of the display. 13 | start_y (int): The starting y position of the display. 14 | display (pygame.Surface): The display surface. 15 | """ 16 | self.digits = [ 17 | [Digit(display, start_x + j * 25, start_y + i * 30) for j in range(16)] 18 | for i in range(6) 19 | ] 20 | self.x = start_x 21 | self.y = start_y 22 | 23 | @staticmethod 24 | def flush(): 25 | """Flushes the display.""" 26 | pygame.display.flip() 27 | 28 | def clear(self): 29 | """Clears the display.""" 30 | for row in self.digits: 31 | for dig in row: 32 | dig.update(0) 33 | 34 | def raw2(self, x, y, value, flush=False): 35 | """ 36 | Updates the display with a raw value. 37 | 38 | Args: 39 | x (int): The x position of the digit. 40 | y (int): The y position of the digit. 41 | value (int): The value to update the digit with. 42 | flush (bool): Whether to flush the display. 43 | """ 44 | self.digits[y][x].update(value) 45 | if flush: 46 | self.flush() 47 | 48 | 49 | class Digit: 50 | """Digit class for the seven segment display.""" 51 | 52 | def __init__(self, display, x, y): 53 | """ 54 | Constructor 55 | 56 | Args: 57 | display (pygame.Surface): The display surface. 58 | x (int): The x position of the digit. 59 | y (int): The y position of the digit. 60 | """ 61 | self.display = display 62 | self.x = x 63 | self.y = y 64 | self.draw_line = pygame.draw.line 65 | self.start_x_pos = [ 66 | self.x + 4, 67 | self.x + 3, 68 | self.x + 1, 69 | self.x + 2, 70 | self.x + 16, 71 | self.x + 18, 72 | self.x + 6, 73 | self.x + 18, 74 | ] 75 | self.end_x_pos = [ 76 | self.x + 14, 77 | self.x + 1, 78 | self.x + 0, 79 | self.x + 12, 80 | self.x + 14, 81 | self.x + 16, 82 | self.x + 16, 83 | self.x + 19, 84 | ] 85 | self.start_y_pos = [ 86 | self.y + 10, 87 | self.y + 1, 88 | self.y + 11, 89 | self.y + 20, 90 | self.y + 11, 91 | self.y + 1, 92 | self.y, 93 | self.y + 20, 94 | ] 95 | self.end_y_pos = [ 96 | self.y + 10, 97 | self.y + 9, 98 | self.y + 19, 99 | self.y + 20, 100 | self.y + 19, 101 | self.y + 9, 102 | self.y, 103 | self.y + 20, 104 | ] 105 | for i in range(8): 106 | self.draw_line( 107 | self.display, 108 | (255, 0, 0), 109 | (self.start_x_pos[i], self.start_y_pos[i]), 110 | (self.end_x_pos[i], self.end_y_pos[i]), 111 | 2, 112 | ) 113 | # pygame.display.flip() 114 | self.state = 255 115 | 116 | def update(self, value): 117 | """ 118 | Updates the digit with a new value. 119 | 120 | Args: 121 | value (int): The new value to update the digit with. 122 | """ 123 | # 7 Segment bit order: DP-G-F-E-D-C-B-A 124 | # 7 Segment bit order: DP-A-B-C-D-E-F-G 125 | diff = self.state ^ value 126 | for i in range(8): 127 | offset = 1 << i 128 | if diff & offset: 129 | self.draw_line( 130 | self.display, 131 | (255, 0, 0) if value & offset else (0, 0, 0), 132 | (self.start_x_pos[i], self.start_y_pos[i]), 133 | (self.end_x_pos[i], self.end_y_pos[i]), 134 | 2, 135 | ) 136 | self.state = value 137 | -------------------------------------------------------------------------------- /docs/API/Broadcasters.md: -------------------------------------------------------------------------------- 1 | # Broadcasters 2 | 3 | ## MQTT Broadcaster 4 | ::: broadcasters.mqtt 5 | 6 | ## Broadcaster Utils 7 | ::: broadcasters.utils -------------------------------------------------------------------------------- /docs/API/Controllers.md: -------------------------------------------------------------------------------- 1 | # Controllers 2 | 3 | ## Gamepad Controller 4 | ::: controllers.gamepad 5 | 6 | ## Keyboard Controller 7 | ::: controllers.keyboard 8 | 9 | ## MQTT Controller 10 | ::: controllers.mqtt -------------------------------------------------------------------------------- /docs/API/Display.md: -------------------------------------------------------------------------------- 1 | # Display 2 | ::: display.display 3 | ::: display.physical_screen 4 | ::: display.segment_display 5 | ::: display.seven_seg 6 | ::: display.symbols 7 | ::: display.virtual_screen 8 | ::: display.virtual_seven_seg -------------------------------------------------------------------------------- /docs/API/Runners.md: -------------------------------------------------------------------------------- 1 | # Runners 2 | 3 | ## Runners Demo 4 | ::: runners.demo 5 | 6 | ## Runners Kiosk 7 | ::: runners.kiosk 8 | 9 | ## Runners Simulator 10 | ::: runners.simulator 11 | 12 | ## Runners Test 13 | ::: runners.test 14 | 15 | ## Runners Utils 16 | ::: runners.utils -------------------------------------------------------------------------------- /docs/Hardware/Enclosure and feet.md: -------------------------------------------------------------------------------- 1 | ## Enclosure 2 | 3 | The enclosure for the physical SSS is made out of two an acryllic (**NOT POLYCARBONATE**) plexiglass fastened together by 18 x M3 screws in a 20mm standoff. The SVG files in [hw/enclosure](https://github.com/NET-BYU/sss/tree/docs/hw/enclosure) can be loaded onto a plasma/laser cutter to create the pieces. 4 | 5 | ### Front Piece 6 | The shape and the mounting holes are seen in this figure below: 7 | 8 | ![enclosure-front-sss](../assets/enclosure-front-sss.svg) 9 | 10 | ### Back Piece 11 | The shape and the mounting holes are also seen on this piece. The upper group of 4 holes in the center allows for the mounting of a Raspberry Pi. The lower five are mounting holes for the [power and breakout board](Power%20Board.md). The slender slots allow for [resistors soldered to the `clk` and `din` pins](Panel.md) to poke out while the larger openings allow for the commnication and power cable to plug into the SSS's panels. 12 | 13 | ![enclosure-back-sss](../assets/enclosure-back-sss.svg) 14 | 15 | ## Feet 16 | 17 | The feet for the SSS are 3D printed pieces that allow for each panel (both front and back) to insert and stand inside it. The more feet are put on the bottom of the enclosure, the more stable it will be. 18 | 19 | 20 | 21 | 28 | 29 | 141 | 142 |
-------------------------------------------------------------------------------- /docs/Hardware/Final product.md: -------------------------------------------------------------------------------- 1 | ## Front of SSS 2 | 3 | ![sss-front](../assets/sss-front.jpg) 4 | 5 | ## Back of SSS 6 | 7 | ![sss-back](../assets/sss-back.jpg) 8 | 9 | ## SSS in Action 10 | 11 | ![sss-lab](../assets/sss-lab.jpg) -------------------------------------------------------------------------------- /docs/Hardware/Panel.md: -------------------------------------------------------------------------------- 1 | The panels of the SSS are the crux of its hardware. If you are going to attempt to replicate the SSS, it is recommended that you look at the the [Known Problems](#known-problems) and address some of those issues. However, for history and current hardware maintenance sake, we will include the current design of the panels. 2 | 3 | ## Panel PCB Blueprints 4 | 5 | ### Panel Schematics 6 | 7 | Each panel has a group of 12 [MAX7219](https://www.maximintegrated.com/en/products/power/display-power-control/MAX7219.html) led drivers which control 8 seven segment digits. All of the ICs receive the CS, SCLK, VCC, and GND from a common trace. The MOSI coming from the Raspberry Pi enters into the first IC and is passed through all of them by tying the DOUT (output) of the preceding IC to the DIN (input). The start of the bus for the panel (SV1) receives the following from the [power board](Power%20Board.md): 8 | 9 | 1. GND 10 | 2. LOAD (CS) 11 | 3. CLK (SCLK) 12 | 4. DIN (MOSI) 13 | 5. VCC 14 | 15 | Originally, the panels were going to be connected together by connecting the busses by having a corresponding 5 pins out (SV2) to connect to the next panel's 5 pins in (SV1). This design made the traces very long and was susceptible to noise and caused a lot of screen artifacts. Instead, each panel receives info from the [power board](Power%20Board.md) via a cable which plugs into the beginning connector of the bus. 16 | 17 | ![panel-sch-sss](../assets/panel-sch-sss.png) 18 | 19 | _**Not shown in schematic:** each SCLK and DIN pin on the IC has a resistor of 200Ω+ between it and the trace. This is a hack and should be dealt with in a future hardware design version._ 20 | 21 | ### Panel PCB 22 | 23 | The PCB design is a double-sided board. The backside is where the 12 ICs, 12 resistors, 24 capacitors, and 2 cable headers are mounted. The frontside is where the 96 seven segment LED digits are mounted. 24 | 25 | ![panel-pcb-sss](../assets/panel-pcb-sss.png) 26 | 27 | ### Panel 28 | 29 | This is the back view of manufactured panel. Note that the SV1 input from the [power board](Power%20Board.md) is on the right hand side of the board and that the set of output pin pads need not be used. 30 | 31 | ![panel-sss](../assets/panel-sss.png) 32 | 33 | ## Known Problems 34 | 35 | The current version of the SSS panel is flawed. Among the known problems are: 36 | 37 | - The output pins are not needed 38 | - The data and clock traces are very long, thin, and susceptible to noise. Changing the trace widths and paths would be necessary. 39 | - Using some sort of differential grounding might also reduce overall board noise. 40 | 41 | Any efforts to redesign the panels are more than welcome! 42 | -------------------------------------------------------------------------------- /docs/Hardware/Power Board.md: -------------------------------------------------------------------------------- 1 | ## Power Board PCB Blueprints 2 | 3 | The power board is a breakout board which takes the SPI protocol wires from the [Raspberry Pi IO](Raspberry%20Pi%20IO.md) and passes them on to the each of the individual [panels](Panel.md). Along with the SPI protocol wires, this board will also power the SSS panels. 4 | 5 | ### Power Board Schematics 6 | 7 | The power board receives all of the Raspberry Pi IO on its 8 x 2 Raspberry Pi headers and reroutes them to the BOARD_n headers. These BOARD_n pins will connect to the SV1 pins on the panels. There is also a barrel jack on this board where a DC adapter that outputs 5V and 4A will supply power to all of the ICs and LED seven segment digits on the panels of the SSS. 8 | 9 | ![power-board-sch-sss](../assets/power-board-sch-sss.png) 10 | 11 | ### Power Board PCB 12 | ![power-board-pcb-sss](../assets/power-board-pcb-sss.png) 13 | 14 | ### Power Board 15 | ![power-board-sss](../assets/power-board-sss.png) -------------------------------------------------------------------------------- /docs/Hardware/Raspberry Pi IO.md: -------------------------------------------------------------------------------- 1 | At the center of the SSS is the Raspberry Pi, a compact, inexpensive computer that has a very accessible hardware interface. The SSS takes advantage of this interface and connects to all of its [panels](Panel.md) through the Pi's GPIO. Checkout this [link](https://pinout.xyz) to get an interactive guide to the GPIO. 2 | 3 | ## SPI Interface 4 | The communication protocol the SSS uses to draw to all of the panels is the Serial Peripheral Interface (or SPI for short). We won't go into the [exact details](https://www.circuitbasics.com/basics-of-the-spi-communication-protocol/) of how this protocol works, but suffice it to say there are several connections necessary to make this work: 5 | 6 | - **MOSI (Master Out Slave In)**: The actual binary instruction which tell the panel what to draw to the screen. 7 | - **CS (Chip Select)**: This wire has to be on (or high) in order to receive and apply instructions from MOSI. There is one of these for each panel in our project. 8 | - **SCLK (Clock Signal)**: This wire has an oscillating high-low timing signal that helps all the CS wires and MOSI stay in sync with each other. 9 | 10 | ## Device Tree Overlay 11 | As you start exploring the Raspberry Pi GPIO its SPI busses (or groups of pins meant to act as a SPI device) you'll notice they only have 1 or 2 CS pins max. This means that by default, a Raspberry Pi can talk to two separate devices (or in our case, panels) at a time. This becomes a problem considering that the SSS has 12 panels! 12 | 13 | We can overcome this problem by reassigning some of the GPIO pins on the Pi to be CS pins for our SPI bus. To do this, we will need to create a device tree overlay, or a special configuration file which reminds the Raspberry Pi's operating system on startup to make these changes to the pin definitions. 14 | 15 | ### Creating the Device Tree Source file 16 | 17 | For those who are interested to see how this works, please refer to the [official documentation](https://www.raspberrypi.com/documentation/computers/configuration.html#device-trees-overlays-and-parameters) from Raspberry Pi on how to create a valid device tree source (`dts`) file and compile it. [sss.dts](https://raw.githubusercontent.com/NET-BYU/sss/docs/dts/sss.dts) has been provided for those who wish to copy the same configuration as the original SSS or to use it as reference. 18 | 19 | A brief summary of what is accomplished in the file, the reassigning of certain GPIO pins, is depicted in the table below and can be used as reference in creating altered `dts` files: 20 | 21 | #### GPIO to CS for SPI Bus 0 Reassignment 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |
GPIO Pin613192625112162021172722
CS No.234567891011121314
56 | 57 | ### Compiling the `dts` into a `dtbo` 58 | After a correctly formatted `dts` is made, it needs to be compiled into a device tree blob overlay (`dtbo`). This is a binary file is referred to upon boot and makes the appropriate changes in pin definition. 59 | 60 | To compile the `dts` file: 61 | ```bash 62 | dtc -@ -I dts -O dtb -o sss.dtbo sss.dts 63 | ``` 64 | 65 | After the `dtbo` binary is created, it needs to be moved or copied into the directory where GPIO overlays for the Pis are held: 66 | 67 | ```bash 68 | sudo cp sss.dtbo /boot/overlays 69 | ``` 70 | 71 | Finally we will edit the boot config file to let the Pi know that we want to apply the overlay we just created: 72 | 73 | ```bash 74 | sudo nano /boot/config.txt 75 | ``` 76 | 77 | Add this line to the file, notice how the first field of the assigned value to `dtoverlay` is our desired `dtbo` file name and the rest of the `cs_pin` correspond to the table in the previous section. 78 | 79 | ``` 80 | dtoverlay=sss,cs2_pin=6,cs3_pin=13,cs4_pin=19,cs5_pin=26,cs6_pin=25,cs7_pin=1,cs8_pin=12,cs9_pin=16,cs10_pin=20,cs11_pin=21,cs12_pin=17,cs13_pin=27,cs14_pin=22 81 | ``` 82 | 83 | Now reboot your device and the changes should take effect! 84 | 85 | ## Pi Wiring 86 | 87 | In our build of the SSS we connected the GPIO of the Raspberry Pi to a breakout board which distributes the CS signal to its respective panel and the MOSI, SCLK, GND, and VCC signals to the rest. 88 | 89 | drawing -------------------------------------------------------------------------------- /docs/Installation/Install SSS on Mac or Linux.md: -------------------------------------------------------------------------------- 1 | ## Checking Dependencies 2 | 3 | To install the SSS on your Mac or Linux device, you will need some certain dependencies. You can get them from your package manager by doing the following 4 | 5 | >This project only uses `python3`. Check to see if you are using `python3` by: 6 | ```bash 7 | python -V 8 | ``` 9 | If your system has multiple versions of `python` installed, you can force v3 by replacing all `python` and `pip` commands with `python3` and `pip3`. 10 | 11 | ### For Mac 12 | Make sure that you have [homebrew](https://brew.sh/) installed and then run the following: 13 | ```bash 14 | curl https://bootstrap.pypa.io/get-pip.py | python && brew install git virtualenv 15 | ``` 16 | 17 | ### For Linux 18 | If you are using [Ubuntu](https://ubuntu.com/) or any other [Debian](https://debian.org) based Linux distro with the `apt` package manager: 19 | ```bash 20 | sudo apt install git python3-virtualenv 21 | ``` 22 | If you are using [RedHat](https://redhat.com/) or any of its variants that uses `dnf`: 23 | ```bash 24 | sudo dnf install git python-virtualenv 25 | ``` 26 | 27 | ## Setting up SSS 28 | Once you have all the dependencies for your system installed, you can now move onto downloading and setting up the SSS on your system. The following steps should be system-agnostic. 29 | 30 | First we'll get the code from GitHub by cloning the repository: 31 | 32 | ```bash 33 | git clone https://github.com/NET-BYU/sss.git 34 | ``` 35 | 36 | Now we'll move into the folder and make sure our virtual environment is set up: 37 | 38 | ```bash 39 | cd sss # Change to SSS project directory 40 | virtualenv venv # Create a python virtual environment called venv 41 | source venv/bin/activate # Activate the venv environment 42 | ``` 43 | You can now verify that you are inside a virtual environment by `echo`ing out the `VIRTUAL_ENV` variable 44 | 45 | ```bash 46 | echo $VIRTUAL_ENV 47 | ``` 48 | If the `VIRTUAL_ENV` is set (i.e. you get output from the previous command like `/home/christopolise/dev/sss/venv`), this indicates that you are inside your new Python virtual environment. 49 | 50 | >Most shells will visually indicate that you are inside of a virtual environment by changing the prompt. For example, you are using `bash`, your prompt change from `user@computer1$` to `(venv) user@computer1$`. You should notice the prompt of your terminal change to indicate that you are now in your newly created python virtual environment. All of the following steps assume you are within this environment. If you are not, you will install all SSS dependencies system-wide which may conflict with some previous configurations. 51 | 52 | Then you will need to install all the python requirements for the SSS: 53 | 54 | ```bash 55 | python3 -m pip install -r requirements.txt 56 | ``` 57 | 58 | And that's it! Once this has successfully completed, you are ready to run the simulator and start developing for the SSS! For more information, go to [Get Started](../Overview/Get%20started.md). -------------------------------------------------------------------------------- /docs/Installation/Install SSS on Windows.md: -------------------------------------------------------------------------------- 1 | ## Checking Dependencies 2 | 3 | To install the SSS on your Windows device, you will need some certain dependencies. You can get them from your package manager by doing the following 4 | 5 | You will need to install [Python 3](https://www.python.org/downloads/release/python-3106/) on your computer if you don't have it installed already. 6 | 7 | You will also need a `git` on your machine and some way to interface with it. We recommend [Git Bash](https://gitforwindows.org/). 8 | 9 | Once that is succesfully set up, you will need to make sure that `pip` is installed on your machine as well. Enter the following in your cmd: 10 | 11 | ```cmd 12 | curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py 13 | python get-pip.py 14 | ``` 15 | 16 | With `pip` installed we can now install `virtualenv`: 17 | ```cmd 18 | python3 -m pip install --user virtualenv 19 | ``` 20 | 21 | ## Setting up SSS 22 | Once you have all the dependencies for your system installed, you can now move onto downloading and setting up the SSS on your system. 23 | 24 | First we'll get the code from GitHub by cloning the repository. Open up your Git Bash program and make sure that you are in your desired folder for downloading. Then clone the repo: 25 | 26 | ```bash 27 | git clone https://github.com/NET-BYU/sss.git 28 | ``` 29 | 30 | Now with cmd, go to the folder where you cloned the repo. 31 | 32 | ```cmd 33 | cd \sss :: Change to SSS project directory 34 | ``` 35 | 36 | Then we will make sure that we create a virtual environment for all the python dependencies 37 | 38 | ```cmd 39 | python -m venv venv :: Create a python virtual environment called venv 40 | .\venv\Scripts\activate :: Activate the venv environment 41 | ``` 42 | 43 | >You should notice the prompt of your terminal change to indicate that you are now in your newly created python virtual environment. All of the following steps assume you are within this environment. If you are not, you will install all SSS dependencies system-wide which may conflict with some previous configurations. 44 | 45 | Then you will need to install all the python requirements for the SSS: 46 | 47 | ```bash 48 | pip install -r requirements.txt 49 | ``` 50 | 51 | And that's it! Once this has successfully completed, you are ready to run the simulator and start developing for the SSS! For more information, go to [Get Started](../Overview/Get%20started.md). 52 | 53 | ## Running on Windows 54 | It its current state, the SSS cannot autoload all of the demos in kiosk, testing, and simulator mode. This may be fixed in a future release. -------------------------------------------------------------------------------- /docs/News/Competitions.md: -------------------------------------------------------------------------------- 1 | ## BYU IT&CSA 2022 Raspberry Pi Competition 2 | 3 | The SSS was presented at the BYU IT&CSA 2022 Raspberry Pi competition with efforts to convince judges of its practical, educational, and IoT capabilities. Ashton Palacios and Chris Kitras represented the original team of contributors at the event and took first prize. 4 | 5 |
6 | Units of the course. 7 |
Chris Kitras (left) holding a single panel of the SSS and Ashton Palacios (right) holding an NES controller which is one possible input for the SSS.
8 |
9 | 10 | -------------------------------------------------------------------------------- /docs/News/Conferences.md: -------------------------------------------------------------------------------- 1 | ## PyCon US 2023 2 | 3 | The SSS was presented at the PyCon US 2023 conference along with a poster with the aim to show off a hardware system that can be used to teach Python to students and control different hardware interfaces. Many people were excited to see the sign and were delighted that they came all the way to Salt Lake City to get rickrolled in a low resolution. 4 | 5 |
6 | Units of the course. 7 |
Laura Landon (left), Ashton Palacios (center), and Chris Kitras (right) presenting the SSS
8 |
9 | 10 | ### Poster 11 | 12 | 13 |

This browser does not support PDFs. Please download the PDF to view it: Download PDF.

14 | 15 |
16 | -------------------------------------------------------------------------------- /docs/News/SSS in the Wild.md: -------------------------------------------------------------------------------- 1 | - [Doom on a sign made out of seven-segment digits | Reddit](https://www.reddit.com/r/itrunsdoom/comments/yedyg9/doom_on_a_sign_made_out_of_sevensegment_digits/) 2 | 3 | - [Play DOOM on Seven-Segment Displays | Hackaday](https://hackaday.com/2022/10/29/play-doom-on-seven-segment-displays) 4 | 5 | - [They did it: Doom can now be played on 7-segment LCD displays | PCForum.hu (Hungarian)](https://pcforum.hu/hirek/25240/megcsinaltak-mar-7-szegmenses-lcd-kijelzokon-is-jatszhato-a-doom) 6 | 7 | - [Yep, Doom Can Run on Seven-Segment Displays](https://www.hackster.io/news/yep-doom-can-run-on-seven-segment-displays-b42956cb73d2) 8 | 9 | - [PyCon US 2023 report #3 | PyConJP ](https://youtu.be/BMf6OrbfOOQ?t=258) 10 | 11 | - [A Look into the Seven-Segment Sign that Can Run Doom](https://ece.byu.edu/department-news/a-look-into-the-seven-segment-sign-that-can-run-doom) -------------------------------------------------------------------------------- /docs/Overview/About the team.md: -------------------------------------------------------------------------------- 1 | Since its inception at the end of summer in 2021 the list of contributors for the SSS has grown significantly. The first version of the system and winner of the BYU IT&CSA Raspberry Pi 2021 competition was designed and developed by [Dinah Bronson](https://www.linkedin.com/in/dinah-squire-3a84831b5/), [Ashton Palacios](https://www.linkedin.com/in/ashton-palacios/), [Chris Kitras](https://www.linkedin.com/in/christopher-kitras/), and [Phil Lundrigan](https://www.linkedin.com/in/philipbl/). 2 | 3 | Since that first edition, the number of [contributors](https://github.com/NET-BYU/sss/graphs/contributors) has doubled mainly with new researchers from the [BYU NET Lab](https://netlab.byu.edu/people/), each contributing one or more unique demos! 4 | 5 | If you are interested in helping the maintain or develop for the SSS, feel free to open a PR and [get started](../Overview/Get%20started.md) by [creating a demo](../Tutorials/Creating%20a%20demo.md). -------------------------------------------------------------------------------- /docs/Overview/Get started.md: -------------------------------------------------------------------------------- 1 | To get started with the SSS, **you will first need to make sure that it is installed on your machine**. There are guides for both [Mac and Linux](../Installation/Install%20SSS%20on%20Mac%20or%20Linux.md) and [Windows](../Installation/Install%20SSS%20on%20Windows.md). Once you have completed this step, you are ready to start tinkering! 2 | 3 | ## Configuring the Environment 4 | As was seen in the installation guides, most of the python libraries which the SSS depends on have been installed in a virtual environment. You will need to make sure that you have activated this environment before running the SSS or else you are bound to run into errors that claim you are missing dependencies/libraries. 5 | 6 | First, make sure you are in the folder where the SSS code is at (in the terminal), and then: 7 | 8 | ### Mac or Linux 9 | ```bash 10 | source venv/bin/activate 11 | ``` 12 | 13 | ### Windows 14 | ```cmd 15 | .\venv\Scripts\activate 16 | ``` 17 | 18 | ## Modes 19 | We realize most people will never run the SSS on its original hardware and most likely will be tinkering with a simulated version. However, the code supports both execution in the original physical environment and on the simulator. 20 | 21 | Whenever you want to run a command for the SSS that will be simulated, add a **`-s`** flag at the end, else the program will try to run on a physical screen and will most likely crash if you do not have one. 22 | 23 | ### Simulator 24 | Simulator mode opens a window with controls to choose through all of the demos. 25 | 26 | ```bash 27 | python main.py simulator 28 | ``` 29 | 30 | **_This mode is not functional in Windows_** 31 | 32 | ### Kiosk 33 | Kiosk mode can be though of as screen-saver mode. The SSS will cycle through demos that have a `demo_time` other than `None` (i.e. everything but games). To run in this mode: 34 | 35 | ```bash 36 | python main.py kiosk # Don't forget to add "-s" if simulating 37 | ``` 38 | 39 | **_This mode is not functional in Windows_** 40 | 41 | 42 | This mode comes with an optional `--testing` flag to speed up `demo_time`s so that you can cycle through demos faster and catch errors more quickly. 43 | 44 | ### Demo 45 | If you want to execute a specific demo instead of waiting for it in kiosk mode, you can use demo mode! Any demo (including games) are available in this mode and will try to use the keyboard as the default input device: 46 | 47 | ```bash 48 | python main.py demo # Don't forget to add "-s" if simulating 49 | ``` 50 | 51 | ### Testing 52 | Finally, if you simply want to make sure that all the demos in the SSS are running correctly (including yours), run: 53 | 54 | ```bash 55 | python main.py testing 56 | ``` 57 | 58 | **_This mode is not functional in Windows_** 59 | 60 | To execute each demo for a few cycles to make sure everything has initialized and run correctly. 61 | 62 | ## What Next? 63 | Now that you have figured out how to run the SSS in its different modes, why not try making something? Take a look at our Tutorials section, especially [Creating a Demo](../Tutorials/Creating%20a%20demo.md) to see how you can make your own interactive demos. -------------------------------------------------------------------------------- /docs/Overview/Introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | The Seven Segment Sign, or SSS for short, is a purely Python, Raspberry Pi-powered system that drives a screen of 1152 seven segment digits (arranged in a 48 x 24 array). But don't let these low specs fool you! This platform provides a beginner-friendly opportunity for anyone to learn how to address complex design problems with strict limitations. Some of the initial demos we've been able to make work are: 4 | 5 | ### Video Player 6 | 7 | ![video](../assets/sss_video.GIF) 8 | 9 | ### Arcade Games 10 | 11 | ![snek](../assets/snek_video.GIF) 12 | 13 | ### and even Doom! 14 | 15 | ![doom](../assets/doom_video.GIF) 16 | 17 | ## What next? 18 | Even though the physical SSS resides in the [BYU NET Lab](https://netlab.byu.edu), the source code, blueprints, and a simulator are all available on [GitHub](https://www.github.com/NET-BYU/sss) and this wiki! 19 | 20 | Take a look at the following links to help you get started: 21 | 22 | - [Get started](Get%20started.md) 23 | - [Support](../Troubleshooting/Support.md) 24 | 25 | Happy hacking! 26 | -------------------------------------------------------------------------------- /docs/Release Notes.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/docs/Release Notes.md -------------------------------------------------------------------------------- /docs/Simulator/Introduction.md: -------------------------------------------------------------------------------- 1 | ## simulator.py 2 | 3 | This file holds the class definitions to both the individually simulated digit and individually simulated panels. 4 | 5 | ### Digit 6 | 7 | This class controls drawing the individual lines and dot of a seven segment digit. The init parameters consists of a display object, and x and y coordinates. The display object is a pygame surface object. The x and y coordinates are the top left corner of the digit on the passed in pygame surface display object. There is one member function called update. This takes a value that corresponds to each of the seven segments and the dot. The bit order is DP-A-B-C-D-E-F-G. It only updates the values that need to change. 8 | 9 | ### Panel 10 | 11 | This class is all about simulating a hardware panel. It is 16 digits across and 6 digits down. The init function parameters are start_x, start_y, and display. These are the same concept as the Digit class above. The display parameter is a pygame surface object. The start_x and start_y parameters are the x y coordinates of the upper left hand corner on the pygame surface display object. This class has three members to mimic the functionality of the hardware panels. The first is raw2. This function takes x, y, value, and flush parameters. The x and y parameters controls which digit to draw on. The value parameter is value to be written to digit. The bit order is stated above in the Digit class. The flush parameters controls if the change to the digit gets updated with this function call. The flush function will take all of the changes to the digits that haven’t been displayed yet and “flips” it to the screen. The clear function does just that, clears the panel 12 | 13 | ## Standalone_sim.py 14 | 15 | ThIs file holds the Simulator class and instantiation. The Simulator class creates a stand alone simulator of the physical SSS. Many of the methods in the class are to only be used internally and wouldn’t be called outside of the class. The init function takes the width, height, and demo_dir parameters. The width and height parameters are the pixel width and height values of the display. These values are 48 and 48. The demo_dir is the directory of where the demos are. This value shouldn’t generally be changed from the inherent value unless your demos are put else where. The start the simulator, instantiate a Simulator object and call the start method. The start method has a pygame control loop that handles the display. The simulator will hot load all the demos in the demo_dir location. It will create a clickable list of demos on the left side of the screen. On the top of this list is a reload button. Demos can be changed while the simulator is running and be hot reloaded. Upon clicking reload, make sure to reclick the demo you want to run to ensure the changes are activated. There is a more button at the bottom of the list. This button will go to the next set of demos is the number of demos is greater than 13. Continue to click on more to cycle through each page of demos. On the bottom of the simulator there is a black box. It may be concerning to see your demo not go all the way to the bottom of the screen. Don’t panic, that is normal. This special area shows when LIVES and SCORE updates are published on the output of your demo. You can input left, right, up, down, and enter commands to your game by pressing the left, right, up, down, enter keys. When you press one of those keys it will send a press command. When that key is released, it will also send a release command. When designing games it is important to remember that these two types of commands get sent. For an example of processing these commands checkout the playable snake and breakout games. -------------------------------------------------------------------------------- /docs/Troubleshooting/Common issues.md: -------------------------------------------------------------------------------- 1 | ## General SSS FAQ 2 | 3 |
4 | Errors stating that there isn't a specific Python module installed when it is 5 |
6 | After the initial installation and running of the program, users may find that upon returning to run the program again, they are met with errors that state they do not have the right Python Modules installed. There are several reasons that could cause this: 7 | 8 |
9 |

Not using the venv for SSS

10 | Make sure that you are running the SSS inside the virtual environment by running: 11 | 12 | ```bash 13 | which python 14 | ``` 15 | 16 | If the response string does not indicate that the python interpreter is coming from a virtual environment, for example: 17 | 18 | ``` 19 | /usr/bin/python 20 | ``` 21 | 22 | Then make sure to activate the virtual environment created for the SSS: 23 | 24 | ```bash 25 | source ~/sss/venv/bin/activate # Make sure this is the path to your venv 26 | ``` 27 | 28 | Then check the python interpreter being used again. It should look something like the following: 29 | 30 | ```bash 31 | which python 32 | 33 | ./venv/bin/python 34 | ``` 35 | 36 |

Installed python modules in wrong environment

37 | During the setup process of the SSS, it is possible that you may not have entered into the virtual environment made for the project before installing the dependencies found in requirements.txt. To see if these modules exist in your virtual environment: 38 | 39 | First activate the created environment: 40 | ```bash 41 | source ~/sss/venv/bin/activate # Make sure this is the path to your venv 42 | ``` 43 | 44 | Check the list of installed modules in the venv: 45 | ```bash 46 | pip freeze # Shows list of python modules installed in venv 47 | ``` 48 | 49 | If the list of packages printed includes the dependencies found in requirements.txt, then you should be good and a further issue should be opened on the repository. 50 |

51 | 52 |
53 | 54 | ## Physical SSS FAQ 55 | 56 |
57 | There is no drawing on the screen even though there is terminal output 58 | 59 |
60 | If there is no drawing on the screen but the terminal output from the program indicates no errors, this indicates that there is something wrong with the hardware itself. There are several things you can check to rule out serious damage first: 61 | 62 |
    63 |
  1. Make sure the screen is plugged in. Often times this is the most overlooked problem
  2. 64 |
  3. Restart the SSS hardware. Unplug everything from the wall. Plug in the Raspberry Pi. Log into the Pi's OS. Plug in the screen hardware. Doing the preceding in this order allows the SSS program to detect the ICs in the screen in a determined (uninitialized) state.
  4. 65 |
  5. Check the continuity of the traces between joints in any of the circuits that carry power.
  6. 66 |
67 |

68 |
69 | 70 | 71 |
72 | I plugged in the screen but it is flashing red, random patterns. What do I do? 73 | 74 |
75 | Normally if the screen is flashing red sporadically, this is indicative of the ICs on the screen being in an uncertain state. The best thing to do is the following: 76 | 77 |
    78 |
  1. Unplug everything from the wall.
  2. 79 |
  3. Plug in the Raspberry Pi.
  4. 80 |
  5. Log into the Pi's OS.
  6. 81 |
  7. Plug in the screen hardware.
  8. 82 |
83 | 84 | Doing the preceding in this order allows the ICs to be reset from their undetermined state. 85 |

86 |
87 | 88 |
89 | Why are there random artifacts left on the screen as a demo is running? 90 |
91 | If a demo is executing normally, but then random digits on the SSS start to display unexpected patterns, this is most likely due to the refresh rate of the ICs being too high. Because the traces and wires on the SSS are somewhat long, this means that the signal is more susceptible to delay and noise. The faster the ICs try to refresh at, the more likely it is for the data to miss the SCLK rising window. 92 |
93 | To mititgate this issue, we will need to modify the seven_seg.py file in the display directory. To edit the file use your favorite text editor and open up the sss/display/seven_seg.py file. Then we will need to modify the following line: 94 |
95 | ```python 96 | DEFAULT_BAUDRATE = 3000000 # Refresh rate of the ICs is currently set to 3MHz 97 | ``` 98 | Dropping the DEFAULT_BAUDRATE value is a quick way to clear up artifacts. However, this also comes with the unfortunate side effect of slowing the execution of the SSS down. A more sure-fire way to fix this problem would be to redesign the panels of the screen to minimize the length of the traces and also make the width thicker. Furthermore, a more concerted effort to reduce noise in the overall design would more effectivly mitigate signal corruption. 99 |

100 |
101 | 102 | ## Simulator SSS FAQ 103 |
104 | None of my outputs seem to work with the simulator. 105 |
106 | As of this revision of the SSS, no output handling has been implemented for the simulator, although it is definitely a TODO. 107 |

108 |
109 | 110 | If there are any other questions that you have concerning the use of the SSS project, please feel free to go to the [support](Support.md) page and follow the links to open an issue. -------------------------------------------------------------------------------- /docs/Troubleshooting/Support.md: -------------------------------------------------------------------------------- 1 | # Support 2 | 3 | As of the latest release, the SSS has been solely developed by students inside of the [BYU NET Lab](https://netlab.byu.edu) and participants in the BYU ECEN IMMERSE 2022 program. While we might not have the exact same group to maintain the demos they made over the summer, the current group of students in the lab frequently monitor the [issues](https://www.github.com/NET-BYU/sss/issues) and [pull requests](https://www.github.com/NET-BYU/sss/pulls) on the repository. 4 | 5 | Please feel free to open either and [issue](https://www.github.com/NET-BYU/sss/issues) or [pull request](https://www.github.com/NET-BYU/sss/pulls) at any time! -------------------------------------------------------------------------------- /docs/Tutorials/Adding Output Broadcasters.md: -------------------------------------------------------------------------------- 1 | # Adding Output Broadcasters 2 | 3 | One of the central ideas of the SSS is that any demo can transmit any type of output and as long as it is formatted correctly. Handling output from the SSS is not particularly difficult to do, but requires: 4 | 1. The correct initialization of the output device/service in `__init__.py` in the `broadcasters` module 5 | 2. The actual driver file which contains the logic to handle demo output by reading it in from the correct system queue (this will be it's own separate python file in the `broadcasters` directory). 6 | 7 | ## `broadcasters` module 8 | Much like the `demos` module which contains all of the games and demos the SSS runs, the `broadcasters` module is the central location where all of the output devices/services receive information from the demo's `output_queue` and relay that to the appropriate broadcasters. Just like a normal Python module, all of the initialization for this module takes place in the `__init__.py`. In the `broadcasters` module, the `__init__.py` is where all the broadcasting services are initialized, checked for exceptions, and process the information from the demos' `output_queue`. 9 | 10 | ### Initializing the output broadcaster 11 | In the `__init__.py` file, there is only one function: `start_outputs(system_queue, demo_output_queue)` which initializes all broadcasters and attaches them to the `system_queue` and the `demo_output_queue`. The beginning of this function is where we create a queue that will feed into output device. This is where the output of the `demo_output_queue` will eventually go. We then initialize the runner for our output device/service by declaring a variable and assigning it to the return value of our output driver's `start_processing_output()` function. In some cases, the initialization status of a broadcaster may be provided by a different function. Make sure you wrap the runner declaration inside of a `try/execpt` statement so if it fails, the entire SSS won't crash. In the case that the initialization runs into an exception, be sure to assign the runner to `None`. 12 | 13 | ```python 14 | try: 15 | from . import example # This imports the actual output driver 16 | 17 | example_q = Queue() 18 | 19 | example_runner = example.start_processing_output(system_queue, example_q) 20 | except Exception as e: 21 | example_runner = None 22 | ``` 23 | 24 | ### Polling from `demo_output_queue` 25 | At the bottom of `start_outputs` is an infinite loop which extracts the current values on the `demo_output_queue` and passes them to available broadcaster runners. To ensure that your broadcaster receives its outputs from the output queue for every tick of the program, make sure `next()` is called on the runner: 26 | 27 | ```python 28 | 29 | # Loops through every available output message in the output queue 30 | for payload in utils.get_all_from_queue(demo_output_queue): 31 | 32 | # If broadcaster is successfully initialized, the output will go to the service/device's queue and into the driver 33 | if example_runner: 34 | example_q.put(payload) 35 | next(example_runner) 36 | ``` 37 | 38 | ### The result 39 | The following is a simplified result of what the `__init__.py` file should look like after having set up our `example` output. 40 | ```python 41 | from queue import Queue 42 | 43 | from loguru import logger 44 | 45 | from . import utils 46 | 47 | 48 | def start_outputs(system_queue, demo_output_queue): 49 | try: 50 | logger.info("Loading example output...") 51 | from . import example 52 | 53 | example_q = Queue() 54 | 55 | example_runner = example.start_processing_output(system_queue, example_q) 56 | logger.info("...done") 57 | except Exception as e: 58 | example_runner = None 59 | logger.warning(e) 60 | logger.warning("Reason for broadcaster initialization failure will go here.") 61 | logger.warning("Program will continue to run without this output.") 62 | 63 | # More broadcasters are declared and initialized here 64 | 65 | while True: 66 | 67 | for payload in utils.get_all_from_queue(demo_output_queue): 68 | if example_runner: 69 | example_q.put(payload) 70 | next(example_runner) 71 | 72 | # More output runners are `ticked` through here 73 | 74 | yield 75 | ``` 76 | 77 | ## Output Driver File 78 | The contents of your output driver file varies widely based on the device or service's method of receiving input. The only requirement for each output driver file is a `start_processing_output(system_queue, driver_q)` function which should contain a generator that will read demo output values from the queue created for the driver before each `yield`. Depending on how your broadcasting service/device API works, you should return `None` from `start_processing_output`, throw an exception, or have a distinct function that checks initializability altogether in the case that it cannot start correctly. 79 | 80 | ### `example.py` Driver 81 | Below is an arbitrary example skeleton file of what an output driver could look like. For a more concrete example, look at the the `mqtt` driver. 82 | 83 | ```python 84 | # Example output driver file 85 | from example_device import process_data 86 | 87 | from . import utils 88 | 89 | def start_processing_output(system_queue, example_q): 90 | 91 | while True: 92 | 93 | for item in utils.get_all_from_queue(example_q): 94 | process_data(item) 95 | yield 96 | 97 | ``` 98 | 99 | ## Output Values 100 | Unlike `demo_input_queue` values, there are no set of universally supported output types for the SSS. It is understood that each type of output that gets put on the `demo_output_queue` will vary widely depending on that demo's targeted broadcaster and purpose. However, it is recommended that info that is sent on the output queue be formatted in JSON. 101 | -------------------------------------------------------------------------------- /docs/assets/cam-case-sss.3mf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/docs/assets/cam-case-sss.3mf -------------------------------------------------------------------------------- /docs/assets/cam-lid-sss.3mf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/docs/assets/cam-lid-sss.3mf -------------------------------------------------------------------------------- /docs/assets/camera-pcb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/docs/assets/camera-pcb.png -------------------------------------------------------------------------------- /docs/assets/camera-schematic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/docs/assets/camera-schematic.png -------------------------------------------------------------------------------- /docs/assets/doom_video.GIF: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/docs/assets/doom_video.GIF -------------------------------------------------------------------------------- /docs/assets/enclosure-front-sss.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 41 | 43 | 46 | 50 | 51 | 52 | 56 | 65 | 71 | 77 | 83 | 89 | 95 | 101 | 107 | 113 | 119 | 125 | 131 | 137 | 143 | 149 | 150 | 151 | -------------------------------------------------------------------------------- /docs/assets/feet-sss.3mf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/docs/assets/feet-sss.3mf -------------------------------------------------------------------------------- /docs/assets/panel-pcb-sss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/docs/assets/panel-pcb-sss.png -------------------------------------------------------------------------------- /docs/assets/panel-sch-sss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/docs/assets/panel-sch-sss.png -------------------------------------------------------------------------------- /docs/assets/panel-sss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/docs/assets/panel-sss.png -------------------------------------------------------------------------------- /docs/assets/pi-compet.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/docs/assets/pi-compet.jpeg -------------------------------------------------------------------------------- /docs/assets/power-board-pcb-sss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/docs/assets/power-board-pcb-sss.png -------------------------------------------------------------------------------- /docs/assets/power-board-sch-sss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/docs/assets/power-board-sch-sss.png -------------------------------------------------------------------------------- /docs/assets/power-board-sss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/docs/assets/power-board-sss.png -------------------------------------------------------------------------------- /docs/assets/pycon.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/docs/assets/pycon.pdf -------------------------------------------------------------------------------- /docs/assets/pycon2023.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/docs/assets/pycon2023.jpg -------------------------------------------------------------------------------- /docs/assets/snek_video.GIF: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/docs/assets/snek_video.GIF -------------------------------------------------------------------------------- /docs/assets/sss-back.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/docs/assets/sss-back.jpg -------------------------------------------------------------------------------- /docs/assets/sss-front.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/docs/assets/sss-front.jpg -------------------------------------------------------------------------------- /docs/assets/sss-lab.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/docs/assets/sss-lab.jpg -------------------------------------------------------------------------------- /docs/assets/sss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/docs/assets/sss.png -------------------------------------------------------------------------------- /docs/assets/sss_video.GIF: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/docs/assets/sss_video.GIF -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Welcome to the Seven Segment Sign 2 | 3 | The Seven Segment Sign, or SSS for short, is a purely Python, Raspberry Pi-powered system that drives a screen of 1152 seven segment digits (arranged in a 48 x 24 array). But don't let these low specs fool you! This platform provides a beginner-friendly opportunity for anyone to learn how to address complex design problems with strict limitations. Some of the initial demos we've been able to make work are: 4 | 5 | ### Video Player 6 | 7 | ![video](assets/sss_video.GIF) 8 | 9 | ### Arcade Games 10 | 11 | ![snek](assets/snek_video.GIF) 12 | 13 | ### and even Doom! 14 | 15 | ![doom](assets/doom_video.GIF) 16 | 17 | ## What next? 18 | Even though the physical SSS resides in the [BYU NET Lab](https://netlab.byu.edu), the source code, blueprints, and a simulator are all available on [GitHub](https://www.github.com/NET-BYU/sss) and this wiki! 19 | 20 | Take a look at the following links to help you get started: 21 | 22 | - [Get started](Overview/Get%20started.md) 23 | - [Support](Troubleshooting/Support.md) 24 | 25 | Happy hacking! -------------------------------------------------------------------------------- /dts/sss.dts: -------------------------------------------------------------------------------- 1 | /dts-v1/; 2 | /plugin/; 3 | 4 | / { 5 | compatible = "brcm,bcm2835", "brcm,bcm2836", "brcm,bcm2708", "brcm,bcm2709", "brcm,bcm2710"; 6 | 7 | fragment@0 { 8 | target = <&spi0>; 9 | frag0: __overlay__ { 10 | #address-cells = <1>; 11 | #size-cells = <0>; 12 | pinctrl-0 = <&spi0_pins &spi0_cs_pins>; 13 | status = "okay"; 14 | // cs-gpios = <0>, <0>, <0>, <0>, <0>, <0>, <0>, <0>, <0>, <0>, <&gpio 0 1>, <&gpio 5 1>, <&gpio 6 1>, <&gpio 13 1>, <&gpio 19 1>, <&gpio 26 1>, <&gpio 1 1>, <&gpio 12 1>, <&gpio 16 1>, <&gpio 20 1>; 15 | cs-gpios = <0>, <0>, <&gpio 6 1>, <&gpio 13 1>, <&gpio 19 1>, <&gpio 26 1>, <&gpio 25 1>, <&gpio 1 1>, <&gpio 12 1>, <&gpio 16 1>, <&gpio 20 1>, <&gpio 21 1>, <&gpio 17 1>, <&gpio 27 1>, <&gpio 22 1>; 16 | // cs-gpios = <0>, <0>, <&gpio 0 1>, <&gpio 5 1>; 17 | 18 | spidev@2{ 19 | compatible = "spidev"; 20 | reg = <2>; /* CE2 */ 21 | #address-cells = <1>; 22 | #size-cells = <0>; 23 | spi-max-frequency = <500000>; 24 | }; 25 | 26 | spidev@3{ 27 | compatible = "spidev"; 28 | reg = <3>; /* CE3 */ 29 | #address-cells = <1>; 30 | #size-cells = <0>; 31 | spi-max-frequency = <500000>; 32 | }; 33 | 34 | 35 | spidev@4{ 36 | compatible = "spidev"; 37 | reg = <4>; /* CE4 */ 38 | #address-cells = <1>; 39 | #size-cells = <0>; 40 | spi-max-frequency = <500000>; 41 | }; 42 | 43 | spidev@5{ 44 | compatible = "spidev"; 45 | reg = <5>; /* CE5 */ 46 | #address-cells = <1>; 47 | #size-cells = <0>; 48 | spi-max-frequency = <500000>; 49 | }; 50 | 51 | spidev@6{ 52 | compatible = "spidev"; 53 | reg = <6>; /* CE6 */ 54 | #address-cells = <1>; 55 | #size-cells = <0>; 56 | spi-max-frequency = <500000>; 57 | }; 58 | 59 | 60 | spidev@7{ 61 | compatible = "spidev"; 62 | reg = <7>; /* CE7 */ 63 | #address-cells = <1>; 64 | #size-cells = <0>; 65 | spi-max-frequency = <500000>; 66 | }; 67 | 68 | 69 | spidev@8{ 70 | compatible = "spidev"; 71 | reg = <8>; /* CE8 */ 72 | #address-cells = <1>; 73 | #size-cells = <0>; 74 | spi-max-frequency = <500000>; 75 | }; 76 | 77 | spidev@9{ 78 | compatible = "spidev"; 79 | reg = <9>; /* CE9 */ 80 | #address-cells = <1>; 81 | #size-cells = <0>; 82 | spi-max-frequency = <500000>; 83 | }; 84 | 85 | spidev@10{ 86 | compatible = "spidev"; 87 | reg = <10>; /* CE10 */ 88 | #address-cells = <1>; 89 | #size-cells = <0>; 90 | spi-max-frequency = <500000>; 91 | }; 92 | 93 | spidev@11{ 94 | compatible = "spidev"; 95 | reg = <11>; /* CE11 */ 96 | #address-cells = <1>; 97 | #size-cells = <0>; 98 | spi-max-frequency = <500000>; 99 | }; 100 | 101 | spidev@12{ 102 | compatible = "spidev"; 103 | reg = <12>; /* CE11 */ 104 | #address-cells = <1>; 105 | #size-cells = <0>; 106 | spi-max-frequency = <500000>; 107 | }; 108 | 109 | spidev@13{ 110 | compatible = "spidev"; 111 | reg = <13>; /* CE11 */ 112 | #address-cells = <1>; 113 | #size-cells = <0>; 114 | spi-max-frequency = <500000>; 115 | }; 116 | 117 | spidev@14{ 118 | compatible = "spidev"; 119 | reg = <14>; /* CE11 */ 120 | #address-cells = <1>; 121 | #size-cells = <0>; 122 | spi-max-frequency = <500000>; 123 | }; 124 | 125 | }; 126 | }; 127 | 128 | fragment@1 { 129 | target = <&gpio>; 130 | __overlay__ { 131 | spi0_cs_pins: spi0_cs_pins { 132 | // brcm,pins = <0 5 6 13 19 26 1 12 16 20>; 133 | brcm,pins = <6 13 19 26 25 1 12 16 20 21 17 27 22>; 134 | brcm,function = <1>; /* out */ 135 | }; 136 | }; 137 | }; 138 | 139 | __overrides__ { 140 | cs2_pin = <&frag0>,"cs-gpios:12", <&spi0_cs_pins>,"brcm,pins:0"; 141 | cs3_pin = <&frag0>,"cs-gpios:24", <&spi0_cs_pins>,"brcm,pins:4"; 142 | cs4_pin = <&frag0>,"cs-gpios:36", <&spi0_cs_pins>,"brcm,pins:8"; 143 | cs5_pin = <&frag0>,"cs-gpios:48", <&spi0_cs_pins>,"brcm,pins:12"; 144 | cs6_pin = <&frag0>,"cs-gpios:60", <&spi0_cs_pins>,"brcm,pins:16"; 145 | cs7_pin = <&frag0>,"cs-gpios:72", <&spi0_cs_pins>,"brcm,pins:20"; 146 | cs8_pin = <&frag0>,"cs-gpios:84", <&spi0_cs_pins>,"brcm,pins:24"; 147 | cs9_pin = <&frag0>,"cs-gpios:96", <&spi0_cs_pins>,"brcm,pins:28"; 148 | cs10_pin = <&frag0>,"cs-gpios:108", <&spi0_cs_pins>,"brcm,pins:32"; 149 | cs11_pin = <&frag0>,"cs-gpios:120", <&spi0_cs_pins>,"brcm,pins:36"; 150 | cs12_pin = <&frag0>,"cs-gpios:132", <&spi0_cs_pins>,"brcm,pins:40"; 151 | cs13_pin = <&frag0>,"cs-gpios:144", <&spi0_cs_pins>,"brcm,pins:44"; 152 | cs14_pin = <&frag0>,"cs-gpios:156", <&spi0_cs_pins>,"brcm,pins:48"; 153 | }; 154 | }; -------------------------------------------------------------------------------- /hw/camera/cam-case-sss.3mf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/hw/camera/cam-case-sss.3mf -------------------------------------------------------------------------------- /hw/camera/cam-lid-sss.3mf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/hw/camera/cam-lid-sss.3mf -------------------------------------------------------------------------------- /hw/case/10 Series/Frame Assembly v7.3mf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/hw/case/10 Series/Frame Assembly v7.3mf -------------------------------------------------------------------------------- /hw/case/10 Series/Frame Assembly v7.f3z: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/hw/case/10 Series/Frame Assembly v7.f3z -------------------------------------------------------------------------------- /hw/case/10 Series/TS10-10 v5.3mf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/hw/case/10 Series/TS10-10 v5.3mf -------------------------------------------------------------------------------- /hw/case/10 Series/TS20-50-459 v8.3mf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/hw/case/10 Series/TS20-50-459 v8.3mf -------------------------------------------------------------------------------- /hw/case/10 Series/TS20-50-635 v5.3mf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/hw/case/10 Series/TS20-50-635 v5.3mf -------------------------------------------------------------------------------- /hw/case/Brackets/Big Bracket v3.3mf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/hw/case/Brackets/Big Bracket v3.3mf -------------------------------------------------------------------------------- /hw/case/Brackets/Panel Holder 1 v4.3mf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/hw/case/Brackets/Panel Holder 1 v4.3mf -------------------------------------------------------------------------------- /hw/case/Brackets/Panel Holder 2 v2.3mf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/hw/case/Brackets/Panel Holder 2 v2.3mf -------------------------------------------------------------------------------- /hw/case/Brackets/Small Bracket v3.3mf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/hw/case/Brackets/Small Bracket v3.3mf -------------------------------------------------------------------------------- /hw/case/Brackets/Spacer 1 v4.3mf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/hw/case/Brackets/Spacer 1 v4.3mf -------------------------------------------------------------------------------- /hw/case/Brackets/Spacer 2 v6.3mf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/hw/case/Brackets/Spacer 2 v6.3mf -------------------------------------------------------------------------------- /hw/case/Front Acrylic v13.3mf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/hw/case/Front Acrylic v13.3mf -------------------------------------------------------------------------------- /hw/case/Full Assembly v14.3mf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/hw/case/Full Assembly v14.3mf -------------------------------------------------------------------------------- /hw/case/Full Assembly v14.f3z: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/hw/case/Full Assembly v14.f3z -------------------------------------------------------------------------------- /hw/case/Panels/Single Panel v19.3mf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/hw/case/Panels/Single Panel v19.3mf -------------------------------------------------------------------------------- /hw/case/Pi/BOTTOM BODY v6.3mf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/hw/case/Pi/BOTTOM BODY v6.3mf -------------------------------------------------------------------------------- /hw/case/Pi/TOP BODY v8.3mf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/hw/case/Pi/TOP BODY v8.3mf -------------------------------------------------------------------------------- /hw/case/Power Distribution Board/Power Distribution Board Case v10.3mf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/hw/case/Power Distribution Board/Power Distribution Board Case v10.3mf -------------------------------------------------------------------------------- /hw/case/Power Supply/Bracket v5.3mf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/hw/case/Power Supply/Bracket v5.3mf -------------------------------------------------------------------------------- /hw/case/Power Supply/Power Supply Case v11.3mf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NET-BYU/sss/f0dbca22fa48e14c4c859ed3491d59f22b31df05/hw/case/Power Supply/Power Supply Case v11.3mf -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import click 4 | from loguru import logger 5 | 6 | from runners import demo, kiosk, test, utils 7 | 8 | logger_level = ["ERROR", "WARNING", "SUCCESS", "INFO", "DEBUG", "TRACE"] 9 | 10 | 11 | @click.group() 12 | @click.option("-v", "--verbose", count=True) 13 | def cli(verbose): 14 | """CLI group.""" 15 | 16 | if verbose >= len(logger_level): 17 | print(f"Only {len(logger_level) - 1} verbose flags allowed.") 18 | exit() 19 | 20 | logger.remove() # Remove default logger 21 | logger.add(sys.stderr, level=logger_level[verbose]) # Add new logger back 22 | 23 | 24 | @cli.command(name="simulator") 25 | def run_simulator(): 26 | """CLI command to run simulator.""" 27 | 28 | from runners import simulator # pylint: disable=import-outside-toplevel 29 | 30 | simulator.run() 31 | 32 | 33 | @cli.command(name="kiosk") 34 | @click.option( 35 | "-s", 36 | "--simulate", 37 | is_flag=True, 38 | default=False, 39 | help="Run in simulated environment.", 40 | ) 41 | @click.option( 42 | "-n", 43 | "--new_hardware", 44 | is_flag=True, 45 | default=False, 46 | help="Run on new hardware.", 47 | ) 48 | @click.option( 49 | "testing", 50 | "-t", 51 | "--test", 52 | is_flag=True, 53 | default=False, 54 | help="Run in test mode. This shortens the demo time and user input time " 55 | "for testing purposes.", 56 | ) 57 | def run_kiosk(simulate, new_hardware, testing): 58 | """CLI command to run kiosk.""" 59 | kiosk.run(simulate, testing=testing, new_hardware=new_hardware) 60 | 61 | 62 | @cli.command("demo") 63 | @click.argument( 64 | "name", 65 | type=click.Choice( 66 | sorted([name for name, _ in utils.get_demos()]), case_sensitive=False 67 | ), 68 | ) 69 | @click.option( 70 | "-s", 71 | "--simulate", 72 | is_flag=True, 73 | default=False, 74 | help="Run in simulated environment.", 75 | ) 76 | @click.option( 77 | "-n", 78 | "--new_hardware", 79 | is_flag=True, 80 | default=False, 81 | help="Run on new hardware.", 82 | ) 83 | @click.option( 84 | "testing", 85 | "-t", 86 | "--test", 87 | is_flag=True, 88 | default=False, 89 | help="Run in test mode. This provides feedback for if your demo is " 90 | "running fast enough relative to the set frame rate.", 91 | ) 92 | def run_demo(name, simulate, new_hardware, testing): 93 | """CLI command to run demo.""" 94 | demo.run(name, simulate, new_hardware, testing=testing) 95 | 96 | 97 | @cli.command("test") 98 | def run_test(): 99 | """CLI command to run test.""" 100 | test.run() 101 | 102 | 103 | if __name__ == "__main__": 104 | cli() 105 | -------------------------------------------------------------------------------- /mkdocs-requirements.txt: -------------------------------------------------------------------------------- 1 | mkdocs 2 | mkdocs-autorefs 3 | mkdocs-bootstrap386 4 | mkdocs-bootswatch 5 | mkdocs-git-revision-date-localized-plugin 6 | mkdocs-material 7 | mkdocs-material-extensions 8 | mkdocs-rtd-dropdown 9 | mkdocs-windmill-dark 10 | mkdocstrings[python] 11 | griffe -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | # Project information 2 | site_name: SSS Docs 3 | site_url: https://net-byu.github.io/sss/ 4 | site_author: christopolise 5 | 6 | # Repository 7 | repo_name: NET-BYU/sss 8 | repo_url: https://github.com/NET-BYU/sss 9 | 10 | # Copyright 11 | copyright: Copyright © 2022 BYU NET Lab 12 | 13 | theme: 14 | name: material 15 | custom_dir: overrides 16 | palette: 17 | - scheme: default 18 | primary: red 19 | accent: red 20 | toggle: 21 | icon: material/brightness-7 22 | name: Switch to dark mode 23 | - scheme: slate 24 | primary: red 25 | accent: red 26 | toggle: 27 | icon: material/brightness-4 28 | name: Switch to light mode 29 | 30 | font: 31 | text: Roboto 32 | code: Roboto Mono 33 | favicon: assets/sss.png 34 | icon: 35 | logo: material/snake 36 | 37 | markdown_extensions: 38 | - pymdownx.highlight: 39 | anchor_linenums: true 40 | - pymdownx.inlinehilite 41 | - pymdownx.snippets 42 | - pymdownx.superfences 43 | 44 | edit_uri: edit/dev/docs/ 45 | 46 | plugins: 47 | - mkdocstrings 48 | - search 49 | - git-revision-date-localized: 50 | enable_creation_date: true 51 | -------------------------------------------------------------------------------- /overrides/main.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block header %} 4 | {% set class = "md-header" %} 5 | {% if "navigation.tabs.sticky" in features %} 6 | {% set class = class ~ " md-header--lifted" %} 7 | {% endif %} 8 | 55 |
56 | 143 | {% if "navigation.tabs.sticky" in features %} 144 | {% if "navigation.tabs" in features %} 145 | {% include "partials/tabs.html" %} 146 | {% endif %} 147 | {% endif %} 148 |
149 | {% endblock %} -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | black==24.3.0 2 | click==8.1.3 3 | inputs==0.5 4 | loguru==0.7.2 5 | numpy==2.1.2 6 | opencv-python==4.10.0.84 7 | paho-mqtt==1.6.1 8 | perlin-noise==1.12 9 | pygame 10 | pygame-widgets==1.0.0 11 | PyYAML==6.0.2 12 | spidev==3.5 13 | urllib3==1.26.19 14 | sysv-ipc==1.1.0 15 | -------------------------------------------------------------------------------- /runners/demo.py: -------------------------------------------------------------------------------- 1 | import time 2 | from importlib import import_module 3 | 4 | from loguru import logger 5 | 6 | import broadcasters 7 | import controllers 8 | from runners import utils 9 | 10 | 11 | def run(demo_name, simulate, new_hardware, testing): 12 | """Main function that runs the demo. 13 | 14 | Args: 15 | demo_name (str): Name of the demo to run. 16 | simulate (bool): Whether to simulate the screen or use the physical screen. 17 | testing (bool): Whether to run the demo in testing mode. 18 | 19 | """ 20 | 21 | if simulate: 22 | from display.virtual_screen import ( # pylint: disable=import-outside-toplevel 23 | VirtualScreen, 24 | ) 25 | 26 | logger.debug("Starting virtual screen...") 27 | screen = VirtualScreen() 28 | elif new_hardware: 29 | from display.physical_screen_v2 import ( # pylint: disable=import-outside-toplevel 30 | PhysicalScreen, 31 | ) 32 | 33 | logger.debug("Starting new physical screen...") 34 | screen = PhysicalScreen() 35 | else: 36 | from display.physical_screen import ( # pylint: disable=import-outside-toplevel 37 | PhysicalScreen, 38 | ) 39 | 40 | logger.debug("Starting physical screen...") 41 | screen = PhysicalScreen() 42 | 43 | queues = utils.Queues() 44 | 45 | # Set up the game 46 | logger.info(f"Starting {demo_name}...") 47 | demo_module = import_module(f"demos.{demo_name}.main") 48 | demo = utils.get_demo_cls(demo_module)( 49 | queues.demo_input_queue, queues.demo_output_queue, screen.display 50 | ) 51 | 52 | # Set up state to run game 53 | tick = screen.create_tick(demo.frame_rate) 54 | handle_input = controllers.start_inputs( 55 | queues.system_queue, queues.demo_input_queue 56 | ) 57 | handle_output = broadcasters.start_outputs( 58 | queues.system_queue, queues.demo_output_queue 59 | ) 60 | runner = demo.run() 61 | 62 | # Clear screen 63 | screen.clear() 64 | 65 | try: 66 | while True: 67 | # Process input 68 | next(handle_input) 69 | 70 | # Process output 71 | next(handle_output) 72 | 73 | # Make sure they are not trying to exit 74 | while not queues.system_queue.empty(): 75 | system_event = queues.system_queue.get() 76 | if system_event == "QUIT": 77 | logger.info("Quitting...") 78 | raise KeyboardInterrupt 79 | 80 | # Tick the demo 81 | _tick(runner, demo, testing) 82 | 83 | # Wait for next tick 84 | next(tick) 85 | except KeyboardInterrupt: 86 | logger.debug("Handling keyboard interrupt") 87 | 88 | logger.info("Stopping current demo") 89 | demo.stop() 90 | screen.clear() 91 | 92 | 93 | def _tick(runner, demo, testing): 94 | """Run the demo for one tick. 95 | 96 | Args: 97 | runner (generator): Generator that runs the demo. 98 | demo (Demo): Demo object. 99 | testing (bool): Whether to run the demo in testing mode. 100 | 101 | """ 102 | if testing: 103 | before_time = time.time() 104 | 105 | next(runner) 106 | 107 | if testing: 108 | after_time = time.time() 109 | 110 | run_time = after_time - before_time 111 | if run_time > 1 / demo.frame_rate: 112 | logger.info( 113 | "Demo took too long to run compared to frame rate: " 114 | f"{run_time} > {1 / demo.frame_rate}" 115 | ) 116 | 117 | 118 | if __name__ == "__main__": 119 | run("snake_ai", simulate=True, testing=True) 120 | -------------------------------------------------------------------------------- /runners/test.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from importlib import import_module 3 | from queue import Queue 4 | from unittest.mock import MagicMock 5 | 6 | from loguru import logger 7 | 8 | from runners import utils 9 | 10 | 11 | def test_demo(demo_name, demo_module_name): 12 | """ 13 | Given a demo name and module, it runs the demo for a couple of ticks to 14 | make sure it doesn't crash. 15 | 16 | Args: 17 | demo_name (str): Name of the demo to test. 18 | demo_module_name (str): Name of the module where the demo is located. 19 | 20 | Returns: 21 | bool: Whether the demo passed or not. 22 | """ 23 | logger.info(f"Testing {demo_name}...") 24 | display = MagicMock() 25 | display.y_height = 48 26 | display.x_width = 48 27 | 28 | input_q = Queue() 29 | output_q = Queue() 30 | 31 | # Set up the game 32 | demo_module = import_module(demo_module_name) 33 | demo = utils.get_demo_cls(demo_module)(input_q, output_q, display) 34 | 35 | # Set up state to run game 36 | runner = demo.run() 37 | 38 | try: 39 | for _ in range(10): # Tick the demo a few times 40 | next(runner) 41 | 42 | demo.stop() 43 | except KeyboardInterrupt: 44 | logger.debug("Handling keyboard interrupt") 45 | demo.stop() 46 | return False 47 | except Exception: # pylint: disable=broad-except 48 | logger.exception(f"{demo_name} failed!") 49 | return False 50 | else: 51 | logger.info(f"{demo_name} passed!") 52 | return True 53 | 54 | 55 | def run(): 56 | """Main function that runs the test.""" 57 | 58 | if not all(test_demo(*demo) for demo in utils.get_demos()): 59 | # One of the demos failed 60 | sys.exit(-1) 61 | else: 62 | # All of the demos worked 63 | sys.exit() 64 | 65 | 66 | if __name__ == "__main__": 67 | run() 68 | -------------------------------------------------------------------------------- /runners/utils.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from pathlib import Path 3 | from queue import Queue 4 | 5 | 6 | @dataclass 7 | class Queues: 8 | """Contains all of the queues that the system/demos care about.""" 9 | 10 | system_queue: Queue = Queue() 11 | demo_input_queue: Queue = Queue() 12 | demo_output_queue: Queue = Queue() 13 | 14 | 15 | def get_demos(demo_dir="demos"): 16 | """ 17 | Given a directory, it finds all valid demos. It returns a list of tuples 18 | where the first value is the demo name and the second value is the module 19 | string of the demo. 20 | 21 | Args: 22 | demo_dir (str): Directory where the demos are located. 23 | 24 | Returns: 25 | list: List of tuples with the demo name and the module string. 26 | """ 27 | demo_path = Path(demo_dir) 28 | 29 | # Only import directories 30 | demos = (d for d in demo_path.iterdir() if d.is_dir()) 31 | 32 | # Make sure there is a main in the folder 33 | demos = (d for d in demos if (d / "main.py").exists()) 34 | 35 | # Convert to module notation 36 | demos = ((d.name, str(d).replace("/", ".") + ".main") for d in demos) 37 | 38 | return demos 39 | 40 | 41 | def get_demo_cls(demo_module): 42 | """ 43 | For a given demo module, it gets the demo class. 44 | 45 | Args: 46 | demo_module (module): Module that contains the demo. 47 | 48 | Returns: 49 | class: The demo class. 50 | """ 51 | demo_name = demo_module.__name__.rsplit(".", 2)[-2] 52 | 53 | return getattr( 54 | demo_module, "".join([word.capitalize() for word in demo_name.split("_")]) 55 | ) 56 | --------------------------------------------------------------------------------