From 837a3f09de87bcff272f88deffa1c55a3f9559d2 Mon Sep 17 00:00:00 2001 From: Boyan Date: Thu, 7 Sep 2023 20:09:41 +0200 Subject: [PATCH] Website is pretty much done. Did a bit of off screen mining --- .gitignore | 6 + src/arduino/.vscode/arduino.json | 8 ++ src/arduino/text_display.ino | 105 +++++++++++++++ src/place/manage.py | 22 ++++ src/place/place/__init__.py | 0 src/place/place/asgi.py | 16 +++ src/place/place/settings.py | 124 ++++++++++++++++++ src/place/place/urls.py | 25 ++++ src/place/place/wsgi.py | 16 +++ src/place/website/__init__.py | 0 src/place/website/admin.py | 5 + src/place/website/apps.py | 6 + src/place/website/migrations/__init__.py | 0 src/place/website/models.py | 15 +++ src/place/website/tests.py | 3 + src/place/website/views.py | 155 +++++++++++++++++++++++ 16 files changed, 506 insertions(+) create mode 100644 src/arduino/.vscode/arduino.json create mode 100644 src/arduino/text_display.ino create mode 100755 src/place/manage.py create mode 100644 src/place/place/__init__.py create mode 100644 src/place/place/asgi.py create mode 100644 src/place/place/settings.py create mode 100644 src/place/place/urls.py create mode 100644 src/place/place/wsgi.py create mode 100644 src/place/website/__init__.py create mode 100644 src/place/website/admin.py create mode 100644 src/place/website/apps.py create mode 100644 src/place/website/migrations/__init__.py create mode 100644 src/place/website/models.py create mode 100644 src/place/website/tests.py create mode 100644 src/place/website/views.py diff --git a/.gitignore b/.gitignore index 5d381cc..4e500d8 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,12 @@ __pycache__/ *.py[cod] *$py.class +# Django migrations +migrations/ + +# sqlite3 db +*.sqlite3 + # C extensions *.so diff --git a/src/arduino/.vscode/arduino.json b/src/arduino/.vscode/arduino.json new file mode 100644 index 0000000..f0285df --- /dev/null +++ b/src/arduino/.vscode/arduino.json @@ -0,0 +1,8 @@ +{ + "port": "/dev/ttyS0", + "configuration": "PartitionScheme=default,CPUFreq=240,FlashMode=qio,FlashFreq=80,FlashSize=4M,UploadSpeed=921600,LoopCore=1,EventsCore=1,DebugLevel=none,EraseFlash=none", + "board": "esp32:esp32:esp32da", + "programmer": "esptool", + "sketch": "text_display.ino", + "output": "../ArduinoOutput" +} \ No newline at end of file diff --git a/src/arduino/text_display.ino b/src/arduino/text_display.ino new file mode 100644 index 0000000..785b801 --- /dev/null +++ b/src/arduino/text_display.ino @@ -0,0 +1,105 @@ + +// Example sketch which shows how to display some patterns +// on a 64x32 LED matrix +// + +#include + + +#define PANEL_RES_X 64 // Number of pixels wide of each INDIVIDUAL panel module. +#define PANEL_RES_Y 32 // Number of pixels tall of each INDIVIDUAL panel module. +#define PANEL_CHAIN 1 // Total number of panels chained one to another + +//MatrixPanel_I2S_DMA dma_display; +MatrixPanel_I2S_DMA *dma_display = nullptr; + +const char *firstLine = "obicham te"; +const char *secondLine = "mnogo, bebi"; +const char *thirdLine = "bottom text"; + + +uint16_t myBLACK = dma_display->color565(0, 0, 0); +uint16_t myWHITE = dma_display->color565(255, 255, 255); +uint16_t myRED = dma_display->color565(255, 0, 0); +uint16_t myGREEN = dma_display->color565(0, 255, 0); +uint16_t myBLUE = dma_display->color565(0, 0, 255); + + + +// Input a value 0 to 255 to get a color value. +// The colours are a transition r - g - b - back to r. +// From: https://gist.github.com/davidegironi/3144efdc6d67e5df55438cc3cba613c8 +uint16_t colorWheel(uint8_t pos) { + if(pos < 85) { + return dma_display->color565(pos * 3, 255 - pos * 3, 0); + } else if(pos < 170) { + pos -= 85; + return dma_display->color565(255 - pos * 3, 0, pos * 3); + } else { + pos -= 170; + return dma_display->color565(0, pos * 3, 255 - pos * 3); + } +} + +void drawText(int colorWheelOffset) +{ + + // draw text with a rotating colour + dma_display->setTextSize(1); // size 1 == 8 pixels high + dma_display->setTextWrap(false); // Don't wrap at end of line - will do ourselves + + dma_display->setCursor(0, 0); // start at top left + uint8_t w = 0; + for (w=0; wsetTextColor(colorWheel((w*32)+colorWheelOffset)); + dma_display->print(firstLine[w]); + } + + dma_display->println(); + for (w=9; w<20; w++) { + dma_display->setTextColor(colorWheel((w*32)+colorWheelOffset)); + dma_display->print("="); + } + + dma_display->println(); + + dma_display->setTextColor(dma_display->color444(15,15,15)); + dma_display->println(secondLine); // 10 char max + dma_display->println(thirdLine); +} + + +void setup() { + + // Module configuration + HUB75_I2S_CFG mxconfig( + PANEL_RES_X, // module width + PANEL_RES_Y, // module height + PANEL_CHAIN // Chain length + ); + + + // Display Setup + dma_display = new MatrixPanel_I2S_DMA(mxconfig); + dma_display->begin(); + dma_display->setBrightness8(90); //0-255 + dma_display->clearScreen(); + dma_display->fillScreen(myWHITE); + + // fill the screen with 'black' + dma_display->fillScreen(dma_display->color444(0, 0, 0)); + +} + +uint8_t wheelval = 0; + +void loop() { + + // animate by going through the colour wheel for the first two lines + drawText(wheelval); + wheelval +=1; + + delay(20); + + +} diff --git a/src/place/manage.py b/src/place/manage.py new file mode 100755 index 0000000..7276861 --- /dev/null +++ b/src/place/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'place.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/src/place/place/__init__.py b/src/place/place/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/place/place/asgi.py b/src/place/place/asgi.py new file mode 100644 index 0000000..9ceb452 --- /dev/null +++ b/src/place/place/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for place project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'place.settings') + +application = get_asgi_application() diff --git a/src/place/place/settings.py b/src/place/place/settings.py new file mode 100644 index 0000000..37179b9 --- /dev/null +++ b/src/place/place/settings.py @@ -0,0 +1,124 @@ +""" +Django settings for place project. + +Generated by 'django-admin startproject' using Django 4.2.1. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.2/ref/settings/ +""" + +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-)1r%_dlx4^+o8sg#z8vm-mq+8oslg=v2*(2zpc6-y+$_ntm4%!' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = ["*"] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + "website", +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'place.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'place.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/4.2/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + +# Password validation +# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/4.2/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.2/howto/static-files/ + +STATIC_URL = 'static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/src/place/place/urls.py b/src/place/place/urls.py new file mode 100644 index 0000000..57dada2 --- /dev/null +++ b/src/place/place/urls.py @@ -0,0 +1,25 @@ +""" +URL configuration for place project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/4.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path +from website import views + +urlpatterns = [ + path('admin/', admin.site.urls), + path('board/', views.current_config), + path('board///', views.update_board), +] diff --git a/src/place/place/wsgi.py b/src/place/place/wsgi.py new file mode 100644 index 0000000..b953d0c --- /dev/null +++ b/src/place/place/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for place project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'place.settings') + +application = get_wsgi_application() diff --git a/src/place/website/__init__.py b/src/place/website/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/place/website/admin.py b/src/place/website/admin.py new file mode 100644 index 0000000..4e638fc --- /dev/null +++ b/src/place/website/admin.py @@ -0,0 +1,5 @@ +from django.contrib import admin +from .models import Board, RestrictedIP +# Register your models here. +admin.site.register(Board) +admin.site.register(RestrictedIP) \ No newline at end of file diff --git a/src/place/website/apps.py b/src/place/website/apps.py new file mode 100644 index 0000000..2b099b1 --- /dev/null +++ b/src/place/website/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class WebsiteConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'website' diff --git a/src/place/website/migrations/__init__.py b/src/place/website/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/place/website/models.py b/src/place/website/models.py new file mode 100644 index 0000000..1df0cc3 --- /dev/null +++ b/src/place/website/models.py @@ -0,0 +1,15 @@ +from django.db import models + +class Board(models.Model): + update_time = models.DateTimeField(unique=True, primary_key=True) + configuration = models.JSONField() + + def __str__(self) -> str: + return f"{self.update_time}" + +class RestrictedIP(models.Model): + ip = models.GenericIPAddressField(unique=True, primary_key=True) + time_added = models.DateTimeField() + + def __str__(self) -> str: + return f"{self.ip}" \ No newline at end of file diff --git a/src/place/website/tests.py b/src/place/website/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/src/place/website/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/src/place/website/views.py b/src/place/website/views.py new file mode 100644 index 0000000..4cba090 --- /dev/null +++ b/src/place/website/views.py @@ -0,0 +1,155 @@ +from django.shortcuts import render +from .models import Board, RestrictedIP +from datetime import datetime +from django.http import HttpResponse +from django.core.exceptions import ObjectDoesNotExist +from colour import Color +from pytz import UTC +# Change these for your board +BOARD_X = 64 +BOARD_Y = 32 + +# Options for implementation of board update: +# 1) Serve at a special url which is a OTC generated by the server and the client (Hard-ish) +# 2) Serve at all times and have the client decide when it has changed(Easy) + + + +# Internal validation +def validateBoard(x:int, y:int, board:dict, ip) -> bool: + try: + ip = RestrictedIP.objects.get(ip=ip) + return "RESTRICTED" + except ObjectDoesNotExist: + pass + key_counter = 0 # Not using len for optimization purposes + + for row in board: + # Check if datatype is list + if not isinstance(board[row], list): + return "WRONG Y" + + # If length of list is not equal to given width of matrix, it is incorrect + length = len(board[row]) + if length != x: + return "WRONG LENGTH" + + key_counter += 1 + + # Check if the keys are the same number as y + if key_counter != y: + return "WRONG X" + return "SUCCESS" + +def restrict_or_unrestrict(ip): + try: + ip = RestrictedIP.objects.get(ip=ip) + timedelta = datetime.utcnow().replace(tzinfo=UTC) - ip.time_added + # if banned for more than 1 minute, unban + if timedelta.seconds > 60: + ip.delete() + return "ALLOWED" + return "STILL RESTRICTED" + except ObjectDoesNotExist: + instance = RestrictedIP(ip=ip, time_added=datetime.now()) + instance.save() + return "RESTRICTION" + +# Internal board class +class localBoard: + def __init__(self) -> None: + try: + self.board = Board.objects.latest("update_time") + except ObjectDoesNotExist: + # Create a blank board + blank_config = {int(i): [-1 for _ in range(64)] for i in range(32)} + + # Time in YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ] format + time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + instance = Board(update_time=time, configuration=blank_config) + instance.save() + self.board = Board.objects.latest("update_time") + + def getBoard(self) -> dict: + return dict(self.board.configuration) + + def setBoard(self, new_configuration:dict, ip) -> bool: + # Validate the board + if err := validateBoard(x=BOARD_X, y=BOARD_Y, board=new_configuration, ip=ip) != "SUCCESS": + return err + + # Save the board + instance = Board(update_time=datetime.now(), configuration=new_configuration) + instance.save() + self.board = Board.objects.latest("update_time") + return "SUCCESS" + + def HTML(self) -> str: + board = self.getBoard() + style = """ + + """ + + selected_pixel = """ + + """ + + html = f"Ballin{style}{selected_pixel}" + for row in board: + html += "" + x = 0 + for col in board[row]: + html += f"" + x += 1 + html += "" + html += "

<------ pick ur color

" + return html + +# Publicly visible functions +def update_board(request, x, y, color): + board = localBoard().getBoard() + try: + color = Color(color).hex + except Exception as e: + return HttpResponse(e) + board[str(y)][int(x)] = color + + ip = restrict_or_unrestrict(request.META["REMOTE_ADDR"]) + if ip == "STILL RESTRICTED": + return HttpResponse("RESTRICTED") + res = localBoard().setBoard(board, request.META["REMOTE_ADDR"]) + return HttpResponse(f"board: {str(res)}, ip: {ip}") + +def current_config(request): + html = localBoard().HTML() + return HttpResponse(html) +