[
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\ncover/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\n.pybuilder/\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n#   For a library or package, you might want to ignore these files since the code is\n#   intended to run in multiple environments; otherwise, check them in:\n# .python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n\n# pytype static type analyzer\n.pytype/\n\n# Cython debug symbols\ncython_debug/\n\n# Images\nimages/*.jpg\n!images/image_1.jpg\n!images/image_2.jpg\n\n# Tensorflow\n*.model*\n\n# Mac\n.DS_Store\n"
  },
  {
    "path": "README.md",
    "content": "![main_gif](./tutorial_images/main_gif.gif)\n\nThis guide goes through all the steps to build your own AI art installation, using a button to \nchange the AI artwork displayed on a screen. The main components used in this guide are:\n \n* Nvidia Jetson Xavier NX (GPU-accelerated single-board computer)\n* Screen with HDMI support\n* Button to change artwork\n* Passive infrared sensor to reduce risk of screen burn-in\n\nIt includes how to set up the computer to run an art kiosk (with code), how to build and assemble the control \nbox, how to integrate the button and PIR sensor etc.\n\nIf you have any questions, feel free to contact me on LinkedIn: [https://www.linkedin.com/in/max-fischer-92997281/](https://www.linkedin.com/in/max-fischer-92997281/) \n\nor\n\n![contact](./tutorial_images/contact.png)\n\n![final_gif_1](./tutorial_images/final_ai_installation/final_gif.gif)\n\n# Table of content\n1. [Prepare the computer (Nvidia Jetson Xavier NX Dev Kit)](#prepare-the-computer-(nvidia-jetson-xavier-nx-dev-kit))\n    1. [Install operating system](#install-operating-system)\n    2. [Install base requirements](#install-base-requirements)\n    3. [Install Jetson GPIO](#install-jetson-gpio)\n    4. [Install xscreensaver (optional)](#install-xscreensaver-(optional))\n    5. [Install Jetson stats (optional)](#install-jetson-stats-(optional))\n2. [Install art kiosk](#install-art-kiosk)\n3. [Add your generative code](#add-your-generative-code)\n    1. [kiosk/arteventhandler.py](#kioskarteventhandlerpy)\n    2. [main.py](#mainpy)\n4. [Build the control box](#build-the-control-box)\n    1. [Hand-cut parts](#hand-cut-parts)\n    2. [Cut wood biscuits holes](#cut-wood-biscuits-holes)\n    3. [Glue parts together](#glue-parts-together)\n    4. [Remove visible gaps](#remove-visible-gaps)\n    5. [Add hinges](#add-hinges)\n    6. [Add magnetic lock](#add-magnetic-lock)\n    7. [Milling edges](#milling-edges)\n    8. [Drill PIR sensor hole](#drill-pir-sensor-hole)\n    9. [Cut cable slots](#cut-cable-slots)\n    10. [Vent holes](#vent-holes)\n    11. [Spackling paste and sanding](#spackling-paste-and-sanding)\n    12. [Painting](#painting)\n5. [Build the button box](#build-the-button-box)\n6. [Assemble art installation](#assemble-art-installation)\n    1. [Screen](#screen)\n    2. [Button box](#button-box)\n    3. [Control box](#control-box)\n    4. [Electronic components](#electronic-components)\n        1. [Main power cable and junction box](#main-power-cable-and-junction-box)\n        2. [Samsung One Connect box](#samsung-one-connect-box)\n        3. [Nvidia Jetson's power adapter](#nvidia-jetson's-power-adapter)\n        4. [Nvidia Jetson](#nvidia-jetson)\n        4. [Connect cables](#connect-cables)\n        5. [Button](#button)\n        6. [PIR sensor](#pir-sensor)\n7. [Final AI art installation](#final-ai-art-installation)\n\n# Prepare the computer (Nvidia Jetson Xavier NX Dev Kit)\nThe Nvidia Jetson Xavier NX Development Kit \n([https://www.nvidia.com/en-us/autonomous-machines/embedded-systems/jetson-xavier-nx/](https://www.nvidia.com/en-us/autonomous-machines/embedded-systems/jetson-xavier-nx/)) is a single-board computer\nwith an integrated Nvidia Jetson Xavier NX module ([https://developer.nvidia.com/embedded/jetson-xavier-nx](https://developer.nvidia.com/embedded/jetson-xavier-nx)). It's \ndeveloped by Nvidia for running computationally demanding tasks on edge. Similar to the Raspberry Pi, it has 40 GPIO pins that you can\ninteract with.\n\nThe development kit (version US/JP/TW) includes:\n\n* x1 Nvidia Jetson Xavier NX\n* x1 19.0V/2.37A power adapter\n* x2 Power cables:\n    * Plug type I -> C5\n    * Plug type B -> C5\n* Quick start / Support guide\n\n![xavier_1](./tutorial_images/setup_computer/xavier_1.jpg)\n\n![xavier_2](./tutorial_images/setup_computer/xavier_2.jpg)\n\n## Install operating system\nAs the Raspberry Pi, Jetson Xavier is using a micro-SD card as its hard drive. As far as I know, there's only one \nsupported OS image (Ubuntu) provided by Nvidia.\n\nTo install the OS, you'll need to use a second computer. \n\nStart of by downloading the OS image: [https://developer.nvidia.com/jetson-nx-developer-kit-sd-card-image](https://developer.nvidia.com/jetson-nx-developer-kit-sd-card-image).\nTo be able to download it, you need to sign up for a `NVIDIA Developer Program Membership`. It's free and quite useful \nas you'll get access to the Nvidia Developer forum. \n\nAfter you've downloaded it, unzip it. \n\nTo flash the OS image to the micro-SD card, start of by inserting the micro-SD card into the second computer and list \nthe available disks. Find the disk name of the micro-SD card you just inserted. In my case, it's `/dev/disk2`:\n\n![xavier_4](./tutorial_images/setup_computer/xavier_4.svg)\n\nWhen you've found the name of the micro-SD card, unmount it.\n\n![xavier_5](./tutorial_images/setup_computer/xavier_5.svg)\n\nNow, change your current directory to where you downloaded and un-zipped the OS image.\n\n![xavier_6](./tutorial_images/setup_computer/xavier_6.svg)\n\nTo flash the micro-SD card with the OS image, run the command below. Replace `/dev/disk2` with the disk name of your \nmicro-SD card and replace `sd-blob.img` with the name of the un-zipped image you downloaded. I've sped up the \nanimation, flashing the card usually takes quite a long time (it took ~55 min @ ~4.6 MB/s for me).\n\n![xavier_7](./tutorial_images/setup_computer/xavier_7.svg)\n\nWhen you're done flashing the micro-SD card with the image, you're ready to boot up the Jetson! Remove the SD-card \nfrom the second computer and insert it into the Jetson computer. The SD-slot is found under the Xavier NX Module.\n\n![xavier_8](./tutorial_images/setup_computer/xavier_8.jpg)\n\nThere's no power button, it will boot when you plug in the power cable. After booting and filling in the initial \nsystem configuration, you should see the Ubuntu desktop.\n\n![xavier_9](./tutorial_images/setup_computer/xavier_9.gif)\n\nIf you get stuck during boot-up with an output as below, try to reboot the machine.\n```bash\n[ *** ] (1 of 2) A start job is running for End-user configuration after initial OEM installation...\n```\n\nFull installation guide from Nvidia can be found here: \n[https://developer.nvidia.com/embedded/learn/get-started-jetson-xavier-nx-devkit](https://developer.nvidia.com/embedded/learn/get-started-jetson-xavier-nx-devkit)\n\n## Install base requirements\nUpdate and upgrade apt-get\n\n```\nsudo apt-get update\nsudo apt-get upgrade\n```\nIf asked to choose between `gdm3` and `lightdm`, choose `gdm3`.\n\nReboot before continuing:\n\n```bash\nsudo reboot\n```\n\nAfter reboot, install pip3:\n\n```bash\nsudo apt install python3-pip\n```\n\nInstall virtual environment:\n\n```bash\nsudo apt install -y python3-venv\n```\n\nCreate a virtual environment in the directory `~/venvs` with the name `artkiosk`:\n\n```bash\npython3 -m venv ~/venvs/artkiosk\n```\n\nActivate the virtual environment:\n\n```bash\nsource ~/venvs/artkiosk/bin/activate\n```\n\nInstall python wheel:\n\n```bash\npip3 install wheel\n```\n\n## Install Jetson GPIO\n[Jetson.GPIO](https://github.com/NVIDIA/jetson-gpio) is a Python package developed by Nvidia that works in the same way\nas RPi.GPIO, but for the Jetson family of computers. It enables the user to, through Python code, interact with the GPIO \npinouts on the Jetson computer.\n\nFirst, install the Jetson.GPIO package into the virtual environment:\n\n```bash\npip3 install Jetson.GPIO\n```\n\nThen, set up user permissions to be able to access the GPIOs. Create a new GPIO user group (replace \n`your_user_name`):\n\n```bash\nsudo groupadd -f -r gpio\nsudo usermod -a -G gpio your_user_name\n```\n\nCopy custom GPIO rules (replace `pythonNN` with your Python version):\n\n```bash\nsudo cp venvs/artkiosk/lib/pythonNN/site-packages/Jetson/GPIO/99-gpio.rules /etc/udev/rules.d/\n```\n\nFull installation guide can be found here: [https://github.com/NVIDIA/jetson-gpio#installation](https://github.com/NVIDIA/jetson-gpio#installation)\n\n## Install xscreensaver (optional)\nTo reduce the risk of burn-in when displaying static art on the screen, a PIR (passive infrared) sensor was integrated. \nWhen no movement has been registered around the art installation, a screen saver was triggered.\n\nThe default screen saver on Ubuntu is `gnome-screensaver`. It's not a screen saver in the \"traditional sense\". Instead of \nshowing moving images, it blanks the screen, basically shutting down the HDMI signals to the screen, enabling the screen to fall into low energy mode.\n\nThe screen I used in this project was a Samsung The Frame 32\" (2020). When the screen was set to HDMI (1/2) and no HDMI \nsignal was provided, it showed a static image telling the user that no HDMI signal is found. This is an unwanted behaviour in this set up, as we either \nwants the screen to go blank, or show some kind of a moving image, to reduce the risk of burn-in. We do not want to see \na new static screen telling us that no hdmi signal is found.\n\nTo solve this problem, `xscreensaver` was installed instead. It's an alternative screen saver that support moving \nimages. Also, it seems like `xscreensaver's` blank screen mode works differently than `gnome-screensaver`. When \n`xscreensaver's` blank screen is triggered, it doesn't seems to shut down the HDMI signal, but rather turn the screen \nblack. This is the behaviour we want in this installation. \n\nIf you're experiencing the same challenge as I did with the screen saver, follow these steps \nto uninstall `gnome-screensaver` and install `xscreensaver`:\n\n```bash\nsudo apt-get remove gnome-screensaver\nsudo apt-get install xscreensaver xscreensaver-data-extra xscreensaver-gl-extra\n```\nAfter uninstalling `gnome-screensaver` and installing `xscreensaver`, it was added to `Startup Applications`:\n\n![screen_saver_installation_1](./tutorial_images/setup_computer/screen_saver_installation_1.png)\n\n![screen_saver_installation_2](./tutorial_images/setup_computer/screen_saver_installation_2.png)\n\n\nFull installation guide: [https://askubuntu.com/questions/292995/configure-screensaver-in-ubuntu/293014#293014](https://askubuntu.com/questions/292995/configure-screensaver-in-ubuntu/293014#293014)\n\n## Install Jetson stats (optional)\n[Jetson stats](https://github.com/rbonghi/jetson_stats) is a really useful open-source package to monitor and control \nthe Jetson. It enables you to track CPU/GPU/Memory usage, check temperatures, increase the swap memory etc.\n\nTo install Jetson stats:\n\n```bash\nsudo -H pip install -U jetson-stats\n```\n\nReboot your machine:\n\n```bash\nsudo reboot\n```\n\nActivate the virtual environment again after reboot:\n\n```bash\nsource ~/venvs/artkiosk/bin/activate\n```\n\nTo check CPU/GPU/Memory usage etc:\n\n```bash\njtop\n```\n\nFull list of commands can be found here: [https://github.com/rbonghi/jetson_stats](https://github.com/rbonghi/jetson_stats)\n\n# Install art kiosk\nWe're now ready to install the art kiosk on the computer! \n\nStart by clone this repository:\n\n```bash\ngit clone https://github.com/maxvfischer/DIY-ai-art.git\n```\n\nChange active directory and install the dependencies:\n\n```bash\ncd DIY-ai-art\npip3 install -r requirements.txt\n```\n\nThe art kiosk is started by executing:\n\n```bash\npython3 -m main\n```\n\nNOTE: The art kiosk will **NOT** work properly if you don't attach the button and the PIR sensor. Please continue to \nfollow the instructions.\n\nThe program running the art kiosk is written in `Python` \nand is running as 4 parallel processes, each implemented as its own class: `Kiosk`, `ArtButton`, `PIRSensorScreensaver` \nand `GANEventHandler`. The entry point is `main.py` and all the parameters used are defined in `config.yaml` (e.g. path to \nimage directory, GPIO pinouts used etc).\n\n![screen_saver_installation_1](./tutorial_images/install_art_kiosk/art_kiosk_diagram.png)\n\n| **Process/Class**              | **File**                   | **Description**                                                                                                                                                                                                                                                                                                                                                                                      |\n|--------------------------------|----------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| **Kiosk**                      | kiosk/kiosk.py             |The `Kiosk` process handles all the GUI: toggling (\\<F11>) and ending (\\<Escape>) fullscreen, listens to change of the active artwork to be displayed etc.                                                                                                                                                                                                                                            |\n| **ArtButton**                  | kiosk/artbutton.py         |The `ArtButton` process listens to a GPIO pinout connected to a button. When the button is pushed and the GPIO is triggered, it replaces the active artwork file with a random image sampled from the image directory.                                                                                                   |\n| **PIRSensorScreensaver**       | kiosk/pirsensorscreensaver |The `PIRSensorScreensaver` process listens to a GPIO pinout connected to a PIR sensor. When no motion has triggered the PIR sensor within a predefined threshold of seconds, the computer's screensaver is activated. When motion is detected, the screensaver is deactivated.                                              |\n| **ArtEventHandler**            | kiosk/art_event_handler.py             |The `ArtEventHandler` process is listening to deleted items in the image directory. When the button is clicked and an image is deleted (i.e. moved to replace the active artwork file, active_artwork.jpg), this process checks how many images that are left in the image directory. If the number of images falls are below a predefined threshold, a new process (function) is spawned, generating a new set of images. You need to update this class to generate the images. |\n\n# Add your generative code\nYou need to add your own generative code (GAN-network or others), by updating two files:\n\n* kiosk/arteventhandler.py\n* main.py\n\n## kiosk/arteventhandler.py\nThe class `ArtEventHandler` found in the file `kiosk/arteventhandler.py` is an event handler that is triggered to generate new images to be saved in the image directory. When an image is deleted from the image directory (i.e. moved to replace the active artwork), `ArtEventHandler's` class function `on_deleted` is executed. It checks if the number of images found in the image directory is above or below a pre-defined threshold. If the number of images falls below the threshold, `ArtEventHandler's` class function `generate_images` is executed. It is in this function that you need to add your generative code that will add new images to the image directory.\n\n## main.py\nIf you have updated `ArtEventHandler's` constructor with new arguments, you need to update the initialization of `ArtEventHandler` in the function `start_art_generator` found in `main.py`.\n\n# Build the control box\nTo get a nice looking installation with as few visible cables as possible, a control box was built to encapsulate the Nvidia computer, power adapters, Samsung One Connect box etc.\n\n## Hand-cut parts\nThe control box was build using 12mm (0.472\") MDF. A vertical panel saw was used to cut down the MDF into smaller pieces. A table saw was used to cut out the final pieces.\n\n![raw_mdf](./tutorial_images/build_control_box/raw_mdf.jpg)\n\n![vertical_panel_saw](./tutorial_images/build_control_box/vertical_panel_saw.jpg)\n\n![table_saw](./tutorial_images/build_control_box/table_saw.jpg)\n\n| Piece              | Dimensions (width, height)    | Sketch                                                                         |\n|--------------------|-------------------------------|--------------------------------------------------------------------------------|\n| Bottom base panel  | 320mm x 235mm                 | ![table_saw](./tutorial_images/build_control_box/bottom_base_panel_sketch.png) |\n| Top lid panel      | 344mm x 259mm                 | ![table_saw](./tutorial_images/build_control_box/top_lid_panel_sketch.png)     |\n| Left side panel    | 235mm x 57mm                  | ![table_saw](./tutorial_images/build_control_box/left_side_panel_sketch.png)   |\n| Right side panel   | 235mm x 57mm                  | ![table_saw](./tutorial_images/build_control_box/right_side_panel_sketch.png)  |\n| Top side panel     | 344mm x 57mm                  | ![table_saw](./tutorial_images/build_control_box/top_side_panel_sketch.png)    |\n| Bottom side panel  | 344mm x 57mm                  | ![table_saw](./tutorial_images/build_control_box/bottom_side_panel_sketch.png) |\n\n![raw_pieces](./tutorial_images/build_control_box/raw_pieces.jpg)\n\n![raw_pieces_with_lid](./tutorial_images/build_control_box/raw_pieces_with_lid.jpg)\n\n## Cut wood biscuits holes\nTo make the control box robust, wood biscuits were used to glue the parts together. By using wood biscuits, \nno screws were needed, thus giving a nice finish without visible screw heads. It also helps to aligning the\npieces when gluing.\n\nWhen using the wood biscuit cutter, it's important that the holes end up at the correct place at the \naligning panels. One simple way of solving this is to align your panels and then draw a line on both \npanels at the center of where you want the biscuit to be. If you do this, the holes will end up at the \nright place.\n\n![wood_biscuit_align](./tutorial_images/build_control_box/wood_biscuit_align.jpg)\n\n![wood_biscuit_machine_1](./tutorial_images/build_control_box/wood_biscuit_machine_1.jpg)\n\n![wood_biscuit_machine_2](./tutorial_images/build_control_box/wood_biscuit_machine_2.jpg)\n\n![wood_biscuit_all_pieces](./tutorial_images/build_control_box/wood_biscuit_all_pieces.jpg)\n\nBefore gluing the pieces together, check that the connecting holes are correctly aligning and that all\nwood biscuits fit nicely (they can somethings vary a bit in size).\n\n![wood_biscuit_asseble](./tutorial_images/build_control_box/wood_biscuit_asseble.jpg)\n\n## Glue parts together\nWhen gluing the parts together, you'll need to be fairly quick and structured. Prepare by placing the \naligning panels next to each other and have all the wood biscuits ready.\n\n![gluing_1](./tutorial_images/build_control_box/gluing_1.jpg)\n\n![gluing_2](./tutorial_images/build_control_box/gluing_2.jpg)\n\nStart of by adding the glue in the wood biscuit holes.\n\n![gluing_3](./tutorial_images/build_control_box/gluing_3.jpg)\n\nThen, press down the wood biscuits into the holes and apply wood glue along all the connecting parts.\n\n![gluing_4](./tutorial_images/build_control_box/gluing_4.jpg)\n\nNow, assemble all the connecting parts and apply force using clamps. You should see\nglue seeping out between the panels.\n\n![gluing_5](./tutorial_images/build_control_box/gluing_5.jpg)\n\nUse an engineer's square to check each corner.\n\n![gluing_6](./tutorial_images/build_control_box/gluing_6.jpg)\n\nFinally, remove all the visible redundant glue with a wet paper tissue.\n\n![gluing_7](./tutorial_images/build_control_box/gluing_7.jpg)\n\n![gluing_8](./tutorial_images/build_control_box/gluing_8.jpg)\n\n## Remove visible gaps\nAfter removing the clamps, there were some visible gaps and cracks that needed to be filled.\n\n![spackling_1](./tutorial_images/build_control_box/spackling_1.jpg)\n\n![spackling_2](./tutorial_images/build_control_box/spackling_2.jpg)\n\n![spackling_3](./tutorial_images/build_control_box/spackling_3.jpg)\n\nI used plastic padding (a two component plastic spackling paste) to cover up the gaps and cracks. Be careful with how \nmuch hardener you add, as it will dry very quickly if adding too much.\n\n![spackling_4](./tutorial_images/build_control_box/spackling_4.jpg)\n\n![spackling_5](./tutorial_images/build_control_box/spackling_5.jpg)\n\n![spackling_6](./tutorial_images/build_control_box/spackling_6.jpg)\n\n![spackling_7](./tutorial_images/build_control_box/spackling_7.jpg)\n\nWhen everything had dried, an electric sander was used to remove redundant plastic padding.\nThe inside of the box was smoothed by manual sanding. As a rule of thumb, if you can\nfeel an edge or a crack, it will be visible when painted.\n\n![spackling_8](./tutorial_images/build_control_box/spackling_8.jpg)\n\n![spackling_9](./tutorial_images/build_control_box/spackling_9.jpg)\n\n## Add hinges\nThe hinges were first added to the lid. It made it easier to align the lid on top of the box \nlater on.\n\nThe hinge mortises were measured and outlined. An electric multicutter tool was then used \nto cut out a grid with the same depth as the hinges. The material was then removed using \na chisel and a hammer. The mortises were then smoothed\nby manual sanding.\n\n![hinge_1](./tutorial_images/build_control_box/hinge_1.jpg)\n\n![hinge_2](./tutorial_images/build_control_box/hinge_2.jpg)\n\n![hinge_3](./tutorial_images/build_control_box/hinge_3.jpg)\n\n![hinge_4](./tutorial_images/build_control_box/hinge_4.jpg)\n\n![hinge_5](./tutorial_images/build_control_box/hinge_5.jpg)\n\n![hinge_6](./tutorial_images/build_control_box/hinge_6.jpg)\n\nThe hinges were aligned and a bradawl was used to mark the centers of the holes. MDF is a \nvery dense material, therefore it's important to pre-drill before screwing the hinges in \nplace. If you don't do this, there's a high risk that the material will crack.\n\n![hinge_7](./tutorial_images/build_control_box/hinge_7.jpg)\n\n![hinge_8](./tutorial_images/build_control_box/hinge_8.jpg)\n\nThe depth of the screws were measured and adhesive tape was used to mark the depth\non the drill head. \n\n![hinge_9](./tutorial_images/build_control_box/hinge_9.jpg)\n\n![hinge_10](./tutorial_images/build_control_box/hinge_10.jpg)\n\n![hinge_11](./tutorial_images/build_control_box/hinge_11.jpg)\n\nBefore aligning the hinges on the box, make sure to add some support under the lid,\nit should be able to rest at the same level as the control box. Double-coated adhesive tape \nwas then attached to each hinge and the lid was aligned on top of the box. When the \nlid was correctly aligned, pressure was applied to make the adhesive tape stick.\n\n![hinge_12](./tutorial_images/build_control_box/hinge_12.jpg)\n\n![hinge_13](./tutorial_images/build_control_box/hinge_13.jpg)\n\n![hinge_14](./tutorial_images/build_control_box/hinge_14.jpg)\n\nThe hinge holes and the mortises were drilled and cut out in the same way as on\nthe lid.\n\n![hinge_15](./tutorial_images/build_control_box/hinge_15.jpg)\n\n![hinge_16](./tutorial_images/build_control_box/hinge_16.jpg)\n\n![hinge_17](./tutorial_images/build_control_box/hinge_17.jpg)\n\n![hinge_18](./tutorial_images/build_control_box/hinge_18.jpg)\n\n![hinge_19](./tutorial_images/build_control_box/hinge_19.jpg)\n\n![hinge_20](./tutorial_images/build_control_box/hinge_20.jpg)\n\n![hinge_21](./tutorial_images/build_control_box/hinge_21.jpg)\n\n![hinge_22](./tutorial_images/build_control_box/hinge_22.jpg)\n\n![hinge_23](./tutorial_images/build_control_box/hinge_23.jpg)\n\n![hinge_24](./tutorial_images/build_control_box/hinge_24.jpg)\n\n## Add magnetic lock\nA magnetic lock was used to keep the lid in place.\n\n![magnetic_lock_1](./tutorial_images/build_control_box/magnetic_lock_1.jpg)\n\n![magnetic_lock_2](./tutorial_images/build_control_box/magnetic_lock_2.jpg)\n\n![magnetic_lock_3](./tutorial_images/build_control_box/magnetic_lock_3.jpg)\n\n![magnetic_lock_4](./tutorial_images/build_control_box/magnetic_lock_4.jpg)\n\n![magnetic_lock_5](./tutorial_images/build_control_box/magnetic_lock_5.jpg)\n\n![magnetic_lock_6](./tutorial_images/build_control_box/magnetic_lock_6.jpg)\n\n![magnetic_lock_7](./tutorial_images/build_control_box/magnetic_lock_7.jpg)\n\n![magnetic_lock_8](./tutorial_images/build_control_box/magnetic_lock_8.jpg)\n\n![magnetic_lock_9](./tutorial_images/build_control_box/magnetic_lock_9.jpg)\n\n## Milling edges\nAll the edges were rounded using a handheld milling machine.\n\n![milling_1](./tutorial_images/build_control_box/milling_1.jpg)\n\n![milling_2](./tutorial_images/build_control_box/milling_2.jpg)\n\n![milling_3](./tutorial_images/build_control_box/milling_3.jpg)\n\n## Drill PIR sensor hole\nTo integrate the PIR sensor, the control box was disassembled. A hole was \nthen measured, aligned and drilled all the way through the lid to enable \nthe PIR reflector to stick out. A larger drill with the same diameter as the \nsensor chip was then used to carefully extend the slot from the inside of the lid. \nThe extended hole was not drilled all the way through, approximately 2 mm was \nleft for the sensor chip to rest on. Finally, a sand paper was used to manually \nsand the edges for a perfect fit.\n\n![pir_sensor_1](./tutorial_images/build_control_box/pir_sensor_1.jpg)\n\n![pir_sensor_2](./tutorial_images/build_control_box/pir_sensor_2.jpg)\n\n![pir_sensor_3](./tutorial_images/build_control_box/pir_sensor_3.jpg)\n\n![pir_sensor_4](./tutorial_images/build_control_box/pir_sensor_4.jpg)\n\n![pir_sensor_5](./tutorial_images/build_control_box/pir_sensor_5.jpg)\n\n![pir_sensor_6](./tutorial_images/build_control_box/pir_sensor_6.jpg)\n\n## Cut cable slots\nTo enable the cables to go in and out of the box, two cable slots were cut out:\n\n1. One cable slot in the top side panel for the One Connect cable and button cables.\n2. One cable slot in the bottom side panel for the electrical cable.\n\nInitially, the cable slots were only cut half way through the top and bottom panels. \nBut I then realized (after I had assembled and painted everything ¯\\\\(ツ)/¯), that it will look much better if I cut the \ncable slots all the way through and then glue a piece of MDF into the hole to cover up the redundant space. That's why \nthe control box is painted in the images below.\n\nA caliper was used to measure the diameter of the cables. An extra ~1mm was then added to the slots for the cables to fit nicely. But in hindsight I would've extended the slot 2-3 mm more.\n\n![caliper](./tutorial_images/build_control_box/caliper.jpg)\n\nThe slots were then outlined at the center of the top and bottom panels. The outlines were also extended approximately \n15 mm into the back panel. A Japanese hand saw/Dozuki saw was used to cut out the slots. A small chisel and a hammer was used to remove the cut out pieces.\n\n![cable_slot_4](./tutorial_images/build_control_box/cable_slot_4.jpg)\n\n![cable_slot_5](./tutorial_images/build_control_box/cable_slot_5.jpg)\n\n![cable_slot_7](./tutorial_images/build_control_box/cable_slot_7.jpg)\n\n![cable_slot_6](./tutorial_images/build_control_box/cable_slot_6.jpg)\n\nA hole saw was used to extract a larger hole in the top of the back panel, connected to the top panel's cable slot. It \nenabled the One Connect Cable to be inserted.\n\n![cable_slot_8](./tutorial_images/build_control_box/cable_slot_8.jpg)\n\nPieces of MDF with the same width and height as the cable slots were cut out. Sand paper was used to do small \nadjustments. The cables were then inserted into the slots and the MDF pieces were aligned and cut to give just enought \nspace for the cables to fit.\n\n![cable_slot_11](./tutorial_images/build_control_box/cable_slot_11.jpg)\n\n![cable_slot_9](./tutorial_images/build_control_box/cable_slot_9.jpg)\n\n![cable_slot_10](./tutorial_images/build_control_box/cable_slot_10.jpg)\n\nWood glue were applied and smeared out on the connecting parts. The MDF pieces were then squeezed \ninto the slots.\n\n![cable_slot_11](./tutorial_images/build_control_box/cable_slot_12.jpg)\n\n![cable_slot_12](./tutorial_images/build_control_box/cable_slot_13.jpg)\n\n![cable_slot_13](./tutorial_images/build_control_box/cable_slot_14.jpg)\n\n## Vent holes\nVent holes were drilled in the bottom and the top panel to enable heat to flow out of the control box.\n\n![vent_holes_1](./tutorial_images/build_control_box/vent_holes_1.jpg)\n\n![vent_holes_2](./tutorial_images/build_control_box/vent_holes_2.jpg)\n\n## Spackling paste and sanding\nPlastic padding were used cover the cracks between the cables slots and the glued MDF pieces. The control box was then \nmanually sanded to remove the redundant plastic padding and round the edges around the vent holes etc. \n\n![spackling_11](./tutorial_images/build_control_box/spackling_11.jpg)\n\n![spackling_12](./tutorial_images/build_control_box/spackling_12.jpg)\n\n![spackling_13](./tutorial_images/build_control_box/spackling_13.jpg)\n\n![spackling_10](./tutorial_images/build_control_box/spackling_10.jpg)\n\n## Painting\nThe control box was painted in the same color as the wall it was attached to. A tip is to buy a color sample can \ninstead of a full can. You will not need a full can, and the sample cans are usually cheaper per litre.\n\nA paint roller was used on the flat areas and a small brush was used to paint the smaller details.\n\n![painting_1](./tutorial_images/build_control_box/painting_1.jpg)\n\n![painting_2](./tutorial_images/build_control_box/painting_2.jpg)\n\n![painting_3](./tutorial_images/build_control_box/painting_3.jpg)\n\n![painting_4](./tutorial_images/build_control_box/painting_4.jpg)\n\n![painting_5](./tutorial_images/build_control_box/painting_5.jpg)\n\nAfter the paint had dried, everything was reassembled.\n\n![painting_6](./tutorial_images/build_control_box/painting_6.jpg)\n\n# Build the button box\nA modified black plastic enclosure box was used as a button box. To integrate the \nbutton, the vertical and horizontal center was first measured and pre-drilled. Then, \na hole with the diameter of the button was drilled.\n\n![button_box_1](./tutorial_images/build_button_box/button_box_1.jpg)\n\n![button_box_2](./tutorial_images/build_button_box/button_box_2.jpg)\n\n![button_box_3](./tutorial_images/build_button_box/button_box_3.jpg)\n\n![button_box_4](./tutorial_images/build_button_box/button_box_4.jpg)\n\n![button_box_5](./tutorial_images/build_button_box/button_box_5.jpg)\n\nAs the button box will be located between the control box and the screen, the two \nbutton cables and the One Connect cable (bringing electricity and HDMI to the screen) \nwill enter the button box at the bottom. The One Connect cable will also exit \nthe button box at the top (continuing to the TV). Two cable slots were therefore extracted at the top\nand the bottom, using a Japanese hand saw/Dozuki saw. The slots were then smoothed\nby manual sanding.\n\n![button_box_6](./tutorial_images/build_button_box/button_box_6.jpg)\n\n![button_box_7](./tutorial_images/build_button_box/button_box_7.jpg)\n\n![button_box_8](./tutorial_images/build_button_box/button_box_8.jpg)\n\nThe screws keeping the enclosure box together were colored black using an aerosol varnish paint. A tip\nwhen painting screws is to stick them into a piece of styrofoam.\n\n![button_box_9](./tutorial_images/build_button_box/button_box_9.jpg)\n\n![button_box_10](./tutorial_images/build_button_box/button_box_10.jpg)\n\n# Assemble art installation\nThe art installation was now ready to be assembled and attached to the wall. A cross line laser was used to vertically align the screen, button box and control box. \n\n## Screen\nThe screen (Samsung The Frame 32\" 2020) was wall-mounted following the instructions included when buying the screen.\n\n![assembly_1](./tutorial_images/assemble_art_installation/assembly_1.jpg)\n\n## Button box\nTwo screw holes were drilled in the bottom plate of the button box. Double-coated adhesive tape was also attached\nto the back side of the bottom plate for further support. The button box was then aligned using the laser and\nattached to the wall using two wall plugs, the two screws and the double-coated adhesive tape.\n\n![assembly_3](./tutorial_images/assemble_art_installation/assembly_3.jpg)\n\n![assembly_6](./tutorial_images/assemble_art_installation/assembly_6.jpg)\n\n![assembly_4](./tutorial_images/assemble_art_installation/assembly_4.jpg)\n\n![assembly_5](./tutorial_images/assemble_art_installation/assembly_5.jpg)\n\n![assembly_7](./tutorial_images/assemble_art_installation/assembly_7.jpg)\n\n![assembly_8](./tutorial_images/assemble_art_installation/assembly_8.jpg)\n\n![assembly_9](./tutorial_images/assemble_art_installation/assembly_9.jpg)\n\n## Control box\nThe control box was attached to the wall using wall plug and two screws. To be able to outline the screw holes, all the electronics were temporarily placed in the control box and two screw holes were outlined and drilled. The HDMI/One Connect\ncable were then inserted into the cable slot and the control box was attached to the wall. \n\n![assembly_10](./tutorial_images/assemble_art_installation/assembly_10.jpg)\n\n![assembly_11](./tutorial_images/assemble_art_installation/assembly_11.jpg)\n\n![assembly_12](./tutorial_images/assemble_art_installation/assembly_12.jpg)\n\n![assembly_13](./tutorial_images/assemble_art_installation/assembly_13.jpg)\n\n## Electronic components\n\n**NOTE: THIS PART INCLUDES WIRING OF HIGH VOLTAGE ELECTRICITY THAT CAN BE\nLETHAL IF NOT DONE PROPERLY. THE COLORS OF THE CABLES CAN VARY DEPENDING ON \nREGION/COUNTRY. BEFORE YOU CONNECT THE POWER CORD TO THE POWER OUTLET, CONSULT WITH A LICENSED ELECTRICIAN TO MAKE SURE THAT EVERYTHING IS PROPERLY WIRED \nAND THAT IT IS IN LINE WITH YOUR LOCAL LEGISLATIONS.**\n\n\n### Main power cable and junction box\n\nThe female side of the main power cord was removed and the cable was inserted\ninto the control box. A junction box was then attach in the bottom right \ncorner using velcro tape. Before the velcro tape was attached, the backside of the junction box was cleaned with denatured alcohol. Three holes were created in the side of the junction box to enable three cables to enter.\n\n![assembly_14](./tutorial_images/assemble_art_installation/assembly_14.jpg)\n\n![assembly_15](./tutorial_images/assemble_art_installation/assembly_15.jpg)\n\n![assembly_16](./tutorial_images/assemble_art_installation/assembly_16.jpg)\n\n![assembly_17](./tutorial_images/assemble_art_installation/assembly_17.jpg)\n\n![assembly_18](./tutorial_images/assemble_art_installation/assembly_18.jpg)\n\n![assembly_19](./tutorial_images/assemble_art_installation/assembly_19.jpg)\n\nA wire stripper was used to strip the jacket/insulation of the power cord, \nas well as the wires inside. A splicing connector (Wago 221, 3-conductor) was then \nattached to each wire, enabling electricity from a single power outlet to be \nsplit to the One Connect Box and to the Nvidia Jetson Xavier NX, without using \na power strip.\n\n![assembly_54](./tutorial_images/assemble_art_installation/assembly_54.jpg)\n\n![assembly_20](./tutorial_images/assemble_art_installation/assembly_20.jpg)\n\n![assembly_21](./tutorial_images/assemble_art_installation/assembly_21.jpg)\n\n![assembly_22](./tutorial_images/assemble_art_installation/assembly_22.jpg)\n\n### Samsung One Connect box\n\nThe Samsung One Connect box was then attached in the left bottom corner using\nvelcro tape. Some margin was left below and to the left of the One Connect box for the power cord and for the PIR sensor cables. Also, the backside of the One Connect Box was cleaned using denatured alcohol before attaching the velcro tape. \n\n![assembly_23](./tutorial_images/assemble_art_installation/assembly_23.jpg)\n\n![assembly_24](./tutorial_images/assemble_art_installation/assembly_24.jpg)\n\nThe screen's power cord (IEC C7 coupler) was inserted into the One Connect \nBox. It was then aligned, measured and cut at an appropriate length to reach \ninside the junction box. The wire stripper was used to remove the jacket/insulation of the power cord, as well as the wires inside. The C7 cable was \nthen inserted into the junction box and connected to the splicing connectors. \nThe ground was left out as the C7 coupler is ungrounded.\n\n![assembly_26](./tutorial_images/assemble_art_installation/assembly_26.jpg)\n\n![assembly_25](./tutorial_images/assemble_art_installation/assembly_25.jpg)\n\n![assembly_27](./tutorial_images/assemble_art_installation/assembly_27.jpg)\n\n### Nvidia Jetson's power adapter\n\nThe power adapter to the Nvidia Jetson Xavier NX was attached in the top left \ncorner using velcro tape. The power cord (IEC C5 coupler) was inserted \ninto the power adapter. It was then aligned, measured and cut at an appropriate \nlength to reach inside the junction box. The wire stripper was used to remove \nthe jacket/insulation of the power cord, as well as the wires inside. The C5 \ncable was then inserted into the junction box and connected to the splicing \nconnectors.\n\n![assembly_28](./tutorial_images/assemble_art_installation/assembly_28.jpg)\n\n![assembly_29](./tutorial_images/assemble_art_installation/assembly_29.jpg)\n\n![assembly_30](./tutorial_images/assemble_art_installation/assembly_30.jpg)\n\n![assembly_31](./tutorial_images/assemble_art_installation/assembly_31.jpg)\n\nBefore closing the junction box, cable ties were tightened around each cable \ngoing into the junction box as strain reliefs.\n\n![assembly_32](./tutorial_images/assemble_art_installation/assembly_32.jpg)\n\n![assembly_55](./tutorial_images/assemble_art_installation/assembly_55.jpg)\n\n### Nvidia Jetson\n\nTo attach the Nvidia Jetson, two pieces of galvanised band was cut out and wrapped \nin insulating tape. The computer was then attached in the top right corner \nusing 4 small screws and washers.\n\n![assembly_56](./tutorial_images/assemble_art_installation/assembly_56.jpg)\n\n![assembly_57](./tutorial_images/assemble_art_installation/assembly_57.jpg)\n\n![assembly_58](./tutorial_images/assemble_art_installation/assembly_58.jpg)\n\n![assembly_33](./tutorial_images/assemble_art_installation/assembly_33.jpg)\n\n![assembly_34](./tutorial_images/assemble_art_installation/assembly_34.jpg)\n\n### Connect cables\n\nThe HDMI, the One Connect Cable and the Xavier NX power cable were connected. \nCable ties were used to structure the cables.\n\n![assembly_35](./tutorial_images/assemble_art_installation/assembly_35.jpg)\n\n![assembly_36](./tutorial_images/assemble_art_installation/assembly_36.jpg)\n\n### Button\n\nThe button changing the artwork was implemented as a pull-up resistor. When the button is \"off\" (i.e. not pushed), a small current will flow from the positive 3.3v, through the resistor and into the GPIO pin, leading to the GPIO pin being HIGH (1). On the other hand, when the button is pushed, the current will flow from the positive 3.3v, through the resistor, via the button and into ground (GND). This will lead to the GPIO pin being LOW (0). This shift in HIGH/LOW on the GPIO pin is registered in the code and is used to change the artwork on the screen. The schema for the pull-up resistor can be found below.\n\n![pull_up_resistor](./tutorial_images/assemble_art_installation/pull-up_resistor.png)\n\nTwo cables were measured and soldered to the button. The cables were then \ninserted into the control box via the same cable slot as the One Connect cable. \nMake sure that you have enough cable to reach to the Nvidia Jetson computer.\n\n![assembly_37](./tutorial_images/assemble_art_installation/assembly_37.jpg)\n\nThe end of a female jumping wire were then soldered to the end of the cable \nconnecting to the ground (for being able to unplug the cable easily). Before \nsoldering the two cables together, one of the cables were passed through a \nshrinking tube. After the cables were soldered together, a blow torch was used \nto shrink the tube around the soldering.\n\n![assembly_38](./tutorial_images/assemble_art_installation/assembly_38.jpg)\n\nA 1kΩ resistor and a female jumping wire were soldered to the other button \ncable. Finally, another female jumping cable were soldered to the other side of \nthe resistor.\n\n![assembly_39](./tutorial_images/assemble_art_installation/assembly_39.jpg)\n\n![assembly_40](./tutorial_images/assemble_art_installation/assembly_40.jpg)\n\n![assembly_41](./tutorial_images/assemble_art_installation/assembly_41.jpg)\n\n![assembly_42](./tutorial_images/assemble_art_installation/assembly_42.jpg)\n\nThe jumping wires were then connected to the following Nvidia Jetson GPIOs:\n\n* Blue: Ground (pin 14)\n* Red: 3.3v (pin 17)\n* Green: GPIO (pin 15)\n\n![button_pinout](./tutorial_images/assemble_art_installation/button_pinout.png)\n\n![assembly_43](./tutorial_images/assemble_art_installation/assembly_43.jpg)\n\nThe Samsung One Connect cable were finally inserted through the button box and the button box's top plate was attached.\n\n![assembly_44](./tutorial_images/assemble_art_installation/assembly_44.jpg)\n\n![assembly_45](./tutorial_images/assemble_art_installation/assembly_45.jpg)\n\nA cable channel was attached to the wall between the button box and the control box using double-coated adhesive tape, fitting the button cables and the One Connect cable. Before it was painted, the cable channel was manually sanded to make a better grip for the color. A primer was then added, followed by two layers of wall paint.\n\n![assembly_60](./tutorial_images/assemble_art_installation/assembly_60.jpg)\n\n![assembly_61](./tutorial_images/assemble_art_installation/assembly_61.jpg)\n\n![assembly_62](./tutorial_images/assemble_art_installation/assembly_62.jpg)\n\n![assembly_63](./tutorial_images/assemble_art_installation/assembly_63.jpg)\n\n![assembly_64](./tutorial_images/assemble_art_installation/assembly_64.jpg)\n\n![assembly_65](./tutorial_images/assemble_art_installation/assembly_65.jpg)\n\n### PIR sensor\n\nThree cables of equal length were measured and cut out (I would've preferred \nto have three different colors, but I only had black and red cable).\n\n![assembly_46](./tutorial_images/assemble_art_installation/assembly_46.jpg)\n\nThree female/female jumping wires were then cut in the middle and soldered \nto the three cables, one set of jumping wires for each cable. To compensate for the bad coloring of the main cables, three different colors were chosen for the \njumping wires.\n\nMultiple larger shrinking tubes were used to keep the three cables together.\n\n![assembly_47](./tutorial_images/assemble_art_installation/assembly_47.jpg)\n\n![assembly_48](./tutorial_images/assemble_art_installation/assembly_48.jpg)\n\nThe PIR sensor I used was a SR602. It has three pinouts that were connected to the Nvidia Jetson:\n\n* **\\-** to GND (pin 6)\n* **\\+** to 3.3v (pin 1)\n* **out** to a GPIO (pin 7)\n\n![pir_pinout](./tutorial_images/assemble_art_installation/pir_pinout.png)\n\nWhen the PIR sensor register a person walking by, **out** will be HIGH. When \nthere's no detection, **out** will be LOW.\n\nThe PIR sensor were then inserted into its slot in the control box lid.\n\n![assembly_59](./tutorial_images/assemble_art_installation/assembly_59.jpg)\n\n![assembly_49](./tutorial_images/assemble_art_installation/assembly_49.jpg)\n\n![assembly_50](./tutorial_images/assemble_art_installation/assembly_50.jpg)\n\n![assembly_51](./tutorial_images/assemble_art_installation/assembly_51.jpg)\n\n![assembly_52](./tutorial_images/assemble_art_installation/assembly_52.jpg)\n\n![assembly_53](./tutorial_images/assemble_art_installation/assembly_53.jpg)\n\n# Final AI art installation\n![final_gif_1](./tutorial_images/final_ai_installation/final_gif.gif)\n\n![final_1](./tutorial_images/final_ai_installation/final_1.jpg)\n\n![final_2](./tutorial_images/final_ai_installation/final_2.jpg)\n\n![final_3](./tutorial_images/final_ai_installation/final_3.jpg)\n\n![final_4](./tutorial_images/final_ai_installation/final_4.jpg)\n\n![final_5](./tutorial_images/final_ai_installation/final_5.jpg)\n\n![final_6](./tutorial_images/final_ai_installation/final_6.jpg)\n\n![final_7](./tutorial_images/final_ai_installation/final_7.jpg)\n\n![final_8](./tutorial_images/final_ai_installation/final_8.jpg)\n"
  },
  {
    "path": "__init__.py",
    "content": ""
  },
  {
    "path": "config.yaml",
    "content": "active_artwork_file_path: 'active_artwork.jpg'\nimage_directory: 'images'\nlower_limit_num_images: 200\n\nkiosk:\n  path: 'frame.png'\n  inner_width: 1710\n  inner_height: 870\n\npirsensor:\n  GPIO_mode: 'BOARD'\n  GPIO_pinout: 7\n  loop_sleep_sec: 0.1\n  screensaver_after_sec: 600\n\nartbutton:\n  GPIO_mode: 'BOARD'\n  GPIO_pinout: 15\n  loop_sleep_sec: 1.0\n"
  },
  {
    "path": "kiosk/__init__.py",
    "content": ""
  },
  {
    "path": "kiosk/art_event_handler.py",
    "content": "import os\nimport multiprocessing\nfrom watchdog.events import FileSystemEventHandler, FileModifiedEvent\n\n\nclass ArtEventHandler(FileSystemEventHandler):\n    \"\"\"\n    Event handler generating new images.\n\n    NOTE: Update this class with your code to generate new images!\n\n    Parameters\n    ----------\n    image_directory : str\n        Path to the image directory to where the newly generated images should be saved.\n\n    lower_limit_num_images : int\n        Lower threshold triggering new images to be generated.\n\n    \"\"\"\n    def __init__(self,\n                 image_directory: str,\n                 lower_limit_num_images: int):\n        self.image_directory = image_directory\n        self.lower_limit_num_images = lower_limit_num_images\n        self.generating_images = multiprocessing.Value('b', False)\n\n    def generate_images(self,\n                        generating_images: multiprocessing.Value) -> None:\n        \"\"\"\n        Generates images to be displayed on the art kiosk.\n\n        This function is executed in its own process when the number of images in the self.image_directory drops\n        below the threshold self.lower_limit_num_images.\n\n        NOTE: Add your generative code in this function, replacing the comments below!\n\n        Parameters\n        ----------\n        generating_images : multiprocessing.Value\n            Multiprocessing value variable to keep track if there are images currently being generated.\n\n        Returns\n        -------\n        None\n        \"\"\"\n        generating_images.value = True\n\n        # GENERATE YOUR IMAGES HERE\n        # For example:\n        # config = tf.ConfigProto(allow_soft_placement=True)\n        # with tf.Session(config=config) as sess:\n        #     gan = StyleGAN(\n        #         sess=sess,\n        #         batch_size=self.batch_size,\n        #         img_size=self.img_size,\n        #         checkpoint_directory=self.checkpoint_directory,\n        #         image_directory=self.image_directory)\n        #\n        #     gan.generate_images(\n        #         num_images=self.test_num\n        #     )\n\n        generating_images.value = False\n\n    def on_deleted(self,\n                   event) -> None:\n        \"\"\"\n        Function triggered when an image is deleted from the image directory. If number of images in\n        self.image_directory falls below the threshold self.lower_limit_num_images, it spawns a new process generating\n        new images.\n\n        Parameters\n        ----------\n        event : -\n            -\n\n        Returns\n        -------\n        None\n        \"\"\"\n        image_names = [image_name for image_name in os.listdir(self.image_directory) if '.jpg' in image_name]\n        num_images = len(image_names)\n        if (num_images < self.lower_limit_num_images) and (self.generating_images.value == False):\n            p_generate = multiprocessing.Process(\n                target=self.generate_images,\n                args=(self.generating_images,)\n            )\n            p_generate.start()\n"
  },
  {
    "path": "kiosk/artbutton.py",
    "content": "import os\nimport sys\nimport time\nimport random\nimport Jetson.GPIO as GPIO\nfrom kiosk.utils import GPIO_MODES\n\n\nclass ArtButton:\n    \"\"\"\n    Listens to GPIO connected button. When clicked, the currently active artwork displayed in the Kiosk is\n    replaced with a randomly sampled image from the image directory. The sampled image is removed from the\n    image directory.\n\n    Parameters\n    ----------\n    GPIO_mode : str\n        GPIO mode used to set up the Nvidia Jetson board. Accepted values: {'BOARD', 'BCM'}\n\n    GPIO_pinout : int\n        GPIO pin number to which the button is connected.\n\n    active_artwork_file_path : str\n        Path to the active artwork file. This is the image that will be displayed in the Kiosk.\n\n    image_directory : str\n        Path to the image directory from where the images will be randomly sampled.\n\n    loop_sleep_sec : float, default=1.0\n        Seconds to sleep after registered button click. Risk of multiple unexpected simultaneous clicks\n        if set to low.\n    \"\"\"\n    def __init__(self,\n                 GPIO_mode: str,\n                 GPIO_pinout: int,\n                 active_artwork_file_path: str,\n                 image_directory: str,\n                 loop_sleep_sec: float = 1.0) -> None:\n        try:\n            mode = GPIO_MODES[GPIO_mode]\n            GPIO.setmode(mode)\n            GPIO.setup(GPIO_pinout, GPIO.IN)\n            self.GPIO_pinout = GPIO_pinout\n        except Exception as e:\n            print(e.message)\n            sys.exit(1)\n\n        if ('.jpg' in active_artwork_file_path) and (os.path.isfile(active_artwork_file_path)):\n            self.active_artwork_file_path = active_artwork_file_path\n        else:\n            raise ValueError('Active arwork file is not a .jpg or does not exist.')\n        if os.path.isdir(image_directory):\n            self.image_directory = image_directory\n\n        self.loop_sleep_sec = loop_sleep_sec\n\n    def _get_random_image_path(self) -> str:\n        \"\"\"\n        Randomly samples a path to an image in the image directory.\n\n        Returns\n        -------\n        str\n            Randomly sampled path to an image.\n        \"\"\"\n        image_names = [image_name for image_name in os.listdir(self.image_directory) if '.jpg' in image_name]\n        image_name = random.choice(image_names)\n\n        image_path = os.path.join(self.image_directory, image_name)\n        return image_path\n\n    def _change_active_artwork(self) -> None:\n        \"\"\"Replaces the currently active artwork image file with a randomly sampled image file from the image directory\"\"\"\n        image_path = self._get_random_image_path()\n        os.rename(\n            src=image_path,\n            dst=self.active_artwork_file_path\n        )\n\n    def _is_false_negative_click(self):\n        \"\"\"Check if false negative click by timeout\"\"\"\n        time.sleep(0.1)\n        input_state = GPIO.input(self.GPIO_pinout)\n        return input_state\n\n    def start(self) -> None:\n        \"\"\"Starts infinate loop listening to button click. When clicked, it changes the active artwork.\"\"\"\n        while True:\n            input_state = GPIO.input(self.GPIO_pinout)\n            if (input_state == False) and (not self._is_false_negative_click()):\n                self._change_active_artwork()\n                time.sleep(self.loop_sleep_sec)\n"
  },
  {
    "path": "kiosk/kiosk.py",
    "content": "import time\nimport PIL.Image\nimport PIL.ImageTk\nfrom tkinter import *\nfrom typing import Optional, Tuple\nfrom watchdog.observers import Observer\nfrom datetime import datetime, timedelta\nfrom watchdog.events import FileSystemEventHandler, FileModifiedEvent\n\n\nclass Kiosk:\n    \"\"\"\n    Kiosk GUI class displaying art on full-screen.\n\n    Parameters\n    ----------\n    active_artwork_path : str\n        Path to active artwork to be displayed. If the active artwork image is updated, the new image will be rendered.\n\n    frame_path : Optional[str], default=None\n        Path to frame image (.jpg or .png)\n\n    frame_inner_size : Optional[Tuple[int, int]], default=None\n        Inner size of frame. Used to resize artwork to fit frame. Only used if `frame_path` is not None.\n    \"\"\"\n    def __init__(self,\n                 active_artwork_path: str,\n                 frame_path: Optional[str] = None,\n                 frame_inner_size: Optional[Tuple[int, int]] = None) -> None:\n        self.tk = Tk()\n        self.tk.attributes('-zoomed', True)\n        self.frame = Frame(self.tk)\n        self.frame.pack()\n        self.label = None\n        self.fullscreen_state = True\n        self.tk.attributes(\"-fullscreen\", self.fullscreen_state)\n        self.tk.bind(\"<F11>\", self._toggle_fullscreen)\n        self.tk.bind(\"<Escape>\", self._end_fullscreen)\n\n        self.active_artwork_path = active_artwork_path\n        self.frame_path = frame_path\n        self.frame_inner_size = frame_inner_size\n        self.image_last_modified = datetime.now()\n\n        self._start_image_event_handler()\n\n    def _start_image_event_handler(self) -> None:\n        \"\"\"Starts watchdog event handler looking for modificaitons to the active artwork image.\"\"\"\n        event_handler = FileSystemEventHandler()\n        event_handler.on_modified = self._on_updated_image\n        observer = Observer()\n        observer.schedule(event_handler, path='.', recursive=False)\n        observer.start() \n\n    def _toggle_fullscreen(self, event: Event = None) -> str:\n        \"\"\"Toggle Tkinter fullscreen state\"\"\"\n        self.fullscreen_state = not self.fullscreen_state\n        self.tk.attributes(\"-fullscreen\", self.fullscreen_state)\n        return \"break\"\n\n    def _end_fullscreen(self, event: Event = None) -> str:\n        \"\"\"End Tkinter fullscreen state\"\"\"\n        self.fullscreen_state = False\n        self.tk.attributes(\"-fullscreen\", False)\n        return \"break\"\n\n    def _image_too_recently_modified(self) -> bool:\n        \"\"\"\n        Check if active artwork image file was too recently modified.\n\n        Returns\n        -------\n        bool\n            If active artwork image file was to recentrly modified.\n        \"\"\"\n        if datetime.now() - self.image_last_modified < timedelta(seconds=1):\n            return True\n        else:\n            return False\n\n    def _on_updated_image(self,\n                          event: FileModifiedEvent) -> None:\n        \"\"\"\n        Re-read active artwork image file and display file.\n\n        Parameters\n        ----------\n        event : FileModifiedEvent\n            Event body from watchdog event handler/observer.\n\n        Return\n        ------\n        None\n        \"\"\"\n        if self._image_too_recently_modified():\n            return\n        time.sleep(0.1)\n        img = self._read_image(\n            img_path=self.active_artwork_path,\n            frame_path=self.frame_path,\n            frame_inner_size=self.frame_inner_size\n        )\n        self.panel.configure(image=img)\n        self.panel.image = img\n        self.image_last_modified = datetime.now()\n\n    @staticmethod\n    def _add_frame_to_image(img: PIL.Image,\n                            frame_path: str,\n                            frame_inner_size: Tuple[int, int]) -> PIL.Image:\n        \"\"\"\n        Add a frame (.jpg/.png) around image.\n\n        Parameters\n        ----------\n        img : PIL.Image\n            Artwork to add frame around.\n\n        frame_path : str\n            Path to frame image.\n\n        frame_inner_size : Tuple[int, int]\n            Inner size of frame. Used to resize artwork to fit frame.\n\n        Returns\n        -------\n        PIL.Image\n            Image with frame around.\n        \"\"\"\n        frame_image = PIL.Image.open(frame_path)\n        img = img.resize(\n            size=frame_inner_size\n        )\n        img_start_point = (\n            (frame_image.size[0]-img.size[0])//2,\n            (frame_image.size[1]-img.size[1])//2\n        )\n        img_end_point = (\n            img_start_point[0]+img.size[0],\n            img_start_point[1]+img.size[1]\n        )\n        frame_image.paste(img, (*img_start_point, *img_end_point))\n        return frame_image\n\n    def _read_image(self, \n                    img_path: str,\n                    frame_path: Optional[str] = None,\n                    frame_inner_size: Optional[Tuple[int, int]] = None) -> PIL.ImageTk.PhotoImage:\n        \"\"\"\n        Reads image to PIL ImageTk PhotoImage.\n\n        Parameters\n        ----------\n        img_path : str\n            Path to image.\n        \n        frame_path : Optional[str], default=None\n            Path to image of frame.\n\n        frame_inner_size : Optional[Tuple[int, int]], default=None\n            (width, height), in pixels, of the inner rectangle of frame. Only used if `frame_path` is not None.\n\n        Returns\n        -------\n        PIL.ImageTk.PhotoImage\n            PIL ImageTk PhotoImage.\n        \"\"\"\n        img = PIL.Image.open(img_path)\n        if frame_path:\n            img = self._add_frame_to_image(\n                img=img,\n                frame_path=frame_path,\n                frame_inner_size=frame_inner_size\n            )\n        img = PIL.ImageTk.PhotoImage(img)\n        return img\n\n    def _setup_image_on_start(self) -> None:\n        \"\"\"Initially setting up and displaying the active artwork image.\"\"\"\n        img = self._read_image(\n            img_path=self.active_artwork_path,\n            frame_path=self.frame_path,\n            frame_inner_size=self.frame_inner_size\n        )\n        self.panel = Label(self.tk, image=img)\n        self.panel.image = img\n        self.panel.pack()\n\n    def start(self) -> None:\n        \"\"\"Start GUI\"\"\"\n        self._setup_image_on_start()\n        self.tk.mainloop()        \n"
  },
  {
    "path": "kiosk/pir_sensor_screensaver.py",
    "content": "import os\nimport sys\nimport time\nimport Jetson.GPIO as GPIO\nfrom datetime import datetime\nfrom kiosk.utils import GPIO_MODES\n\n\nclass PIRSensorScreensaver:\n    \"\"\"\n    Listens to PIR sensor and activates screensaver if no movement.\n\n    Parameters\n    ----------\n    GPIO_mode : str\n        GPIO mode used to set up the Nvidia Jetson board. Accepted values: {'BOARD', 'BCM'}\n\n    GPIO_pinout : int\n        GPIO pin number to which the PIR sensor is connected.\n\n    loop_sleep_sec : float, default=0.1\n        Seconds to sleep when reading PIR sensor and checking screensaver.\n\n    screensaver_after_sec : float, default=10.\n        Seconds before the screensaver will be activated.\n    \"\"\"\n    def __init__(self,\n                 GPIO_mode: str,\n                 GPIO_pinout: int,\n                 loop_sleep_sec: float = 0.1,\n                 screensaver_after_sec: float = 10.):\n        try:\n            mode = GPIO_MODES[GPIO_mode]\n            GPIO.setmode(mode)\n            GPIO.setup(GPIO_pinout, GPIO.IN)\n            self.GPIO_pinout = GPIO_pinout\n        except Exception as e:\n            print(e.message)\n            sys.exit(1)\n        self.loop_sleep_sec = loop_sleep_sec\n        self.screensaver_after_sec = screensaver_after_sec\n        self.datetime_last_pir_firing = datetime.now()\n        self.screensaver_active = False\n\n    def _check_change_pir_sensor(self) -> None:\n        \"\"\"Check PIR sensor for movement. If firing, update datetime of last pir firing.\"\"\"\n        sensor_is_firing = GPIO.input(self.GPIO_pinout)\n        if sensor_is_firing == True:\n           self.datetime_last_pir_firing = datetime.now()\n\n    def _handle_screensaver(self) -> None:\n        \"\"\"Handling if screensaver should be activated/deactivated, depending on PIR sensor.\"\"\"\n        sec_since_pir_firing = (datetime.now() - self.datetime_last_pir_firing).seconds\n\n        # TODO: Fix weird behavior when screensaver is deactivated with keyboard/mouse.\n        if (sec_since_pir_firing > self.screensaver_after_sec) and (not self.screensaver_active):\n            os.popen('xscreensaver-command -activate')\n            self.screensaver_active = True\n        elif (sec_since_pir_firing <= self.screensaver_after_sec) and (self.screensaver_active):\n            os.popen('xscreensaver-command -deactivate')\n            self.screensaver_active = False\n\n    def start(self) -> None:\n        \"\"\"Start PIR Sensor listener\"\"\"\n        while True:\n            self._check_change_pir_sensor()\n            self._handle_screensaver()\n            time.sleep(self.loop_sleep_sec)"
  },
  {
    "path": "kiosk/utils.py",
    "content": "import yaml\nimport Jetson.GPIO as GPIO\n\n\nGPIO_MODES = {\n    'BOARD': GPIO.BOARD,\n    'BCM': GPIO.BCM\n}\n\n\ndef read_yaml(file_path: str) -> dict:\n    \"\"\"\n    Safely reads a yaml-file.\n    \n    Parameters\n    ----------\n    file_path : str\n        File path to YAML-file.\n    \n    Returns\n    -------\n    dict\n        Loaded YAML-file.\n    \"\"\"\n    with open(file_path, 'r') as stream:\n        try:\n            return yaml.safe_load(stream)\n        except yaml.YAMLError as exc:\n            print(exc)\n\n    return {}\n"
  },
  {
    "path": "main.py",
    "content": "import time\nimport multiprocessing\nfrom kiosk.kiosk import Kiosk\nfrom kiosk.utils import read_yaml\nfrom kiosk.artbutton import ArtButton\nfrom watchdog.observers import Observer\nfrom kiosk.art_event_handler import ArtEventHandler\nfrom kiosk.pir_sensor_screensaver import PIRSensorScreensaver\n\n\ndef start_artbutton(GPIO_mode: str,\n                    GPIO_pinout: int,\n                    active_artwork_file_path: str,\n                    image_directory: str,\n                    loop_sleep_sec: float) -> None:\n    \"\"\"\n    Starts the art button listener.\n\n    Parameters\n    ----------\n    GPIO_mode : str\n        GPIO mode used to set up the Nvidia Jetson board. Accepted values: {'BOARD', 'BCM'}\n\n    GPIO_pinout : int\n        GPIO pin number to which the button is connected.\n\n    active_artwork_file_path : str\n        Path to the active artwork file. This is the image that will be displayed in the Kiosk.\n\n    image_directory : str\n        Path to the image directory from where the images will be randomly sampled.\n\n    loop_sleep_sec : float\n        Seconds to sleep after registered button click. Risk of multiple unexpected simultaneous clicks\n        if set to low.\n\n    Returns\n    -------\n    None\n    \"\"\"\n    button = ArtButton(\n        GPIO_mode=GPIO_mode,\n        GPIO_pinout=GPIO_pinout,\n        active_artwork_file_path=active_artwork_file_path,\n        image_directory=image_directory,\n        loop_sleep_sec=loop_sleep_sec\n    )\n    button.start()\n    \n\ndef start_kiosk(active_artwork_file_path: str,\n                frame_path: str,\n                frame_inner_size: tuple) -> None:\n    \"\"\"\n    Starts art kiosk.\n\n    Parameters\n    ----------\n    active_artwork_file_path : str\n        Path to active artwork to be displayed. If the active artwork image is updated, the new image will be rendered.\n\n    frame_path : str\n        Path to frame image.\n\n    frame_inner_size : tuple\n        Inner size of frame. Used to resize artwork to fit frame.\n\n    Returns\n    -------\n    None\n    \"\"\"\n    kiosk = Kiosk(\n        active_artwork_path=active_artwork_file_path, \n        frame_path=frame_path,\n        frame_inner_size=frame_inner_size)\n    kiosk.start()\n\n\ndef start_pir(GPIO_mode: str,\n              GPIO_pinout: int,\n              loop_sleep_sec: float,\n              screensaver_after_sec: float) -> None:\n    \"\"\"\n    Starts passive infrared sensor listener.\n\n    Parameters\n    ----------\n    GPIO_mode : str\n        GPIO mode used to set up the Nvidia Jetson board. Accepted values: {'BOARD', 'BCM'}\n\n    GPIO_pinout : int\n        GPIO pin number to which the PIR sensor is connected.\n\n    loop_sleep_sec : float\n        Seconds to sleep when reading PIR sensor and checking screensaver.\n\n    screensaver_after_sec : float\n        Seconds before the screensaver will be activated.\n\n    Returns\n    -------\n    None\n    \"\"\"\n    pir = PIRSensorScreensaver(\n        GPIO_mode=GPIO_mode,\n        GPIO_pinout=GPIO_pinout,\n        loop_sleep_sec=loop_sleep_sec,\n        screensaver_after_sec=screensaver_after_sec\n    )\n    pir.start()\n\n\ndef start_art_generator(image_directory: str,\n                        lower_limit_num_images: int) -> None:\n    \"\"\"\n    Starts event handler that listens to deleted images in the image_directory. If an image is deleted from the\n    image_directory (i.e. moved to replace the active artwork) and the number of images in image_directory falls\n    below lower_limit_num_images, a process is spawned to generate new images.\n\n    NOTE: You need to update the class ArtEventHandler and pass the needed arguments to generate new images!\n\n    Parameters\n    ----------\n    image_directory : str\n        Path to the image directory to where the newly generated images should be saved.\n\n    lower_limit_num_images : int\n        Lower threshold triggering new images to be generated.\n\n    Returns\n    -------\n    None\n    \"\"\"\n    handler = ArtEventHandler(\n        image_directory=image_directory,\n        lower_limit_num_images=lower_limit_num_images\n    )\n    observer = Observer()\n    observer.schedule(handler, path=image_directory, recursive=False)\n    observer.start()\n\n    while True:\n        time.sleep(1)\n\n\nif __name__ == '__main__':\n    config = read_yaml('config.yaml')\n\n    p_button = multiprocessing.Process(\n        target=start_artbutton,\n        args=(\n            config['artbutton']['GPIO_mode'],\n            config['artbutton']['GPIO_pinout'],\n            config['active_artwork_file_path'],\n            config['image_directory'],\n            config['artbutton']['loop_sleep_sec']\n        )\n    )\n\n    p_kiosk = multiprocessing.Process(\n        target=start_kiosk,\n        args=(\n            config['active_artwork_file_path'],\n            config['kiosk']['path'],\n            (config['kiosk']['inner_width'], config['kiosk']['inner_height'])\n        )\n    )\n\n    p_pir = multiprocessing.Process(\n        target=start_pir,\n        args=(\n            config['pirsensor']['GPIO_mode'],\n            config['pirsensor']['GPIO_pinout'],\n            config['pirsensor']['loop_sleep_sec'],\n            config['pirsensor']['screensaver_after_sec'],\n        )\n    )\n\n    p_art = multiprocessing.Process(\n        target=start_art_generator,\n        args=(\n            config['image_directory'],\n            config['lower_limit_num_images']\n        )\n    )\n\n    p_button.start()\n    p_kiosk.start()\n    p_pir.start()\n    p_art.start()\n\n    p_button.join()\n    p_kiosk.join()\n    p_pir.join()\n    p_art.join()\n"
  },
  {
    "path": "requirements.txt",
    "content": "pillow==8.1.2\ntqdm==4.32.1\nwatchdog==0.10.3\nJetson.GPIO==2.0.12\npyyaml==5.4\n"
  }
]