Website is pretty much done. Did a bit of off screen mining

This commit is contained in:
Boyan 2023-09-07 20:09:41 +02:00
parent 2dd3b711f7
commit 837a3f09de
16 changed files with 506 additions and 0 deletions

6
.gitignore vendored
View File

@ -4,6 +4,12 @@ __pycache__/
*.py[cod]
*$py.class
# Django migrations
migrations/
# sqlite3 db
*.sqlite3
# C extensions
*.so

8
src/arduino/.vscode/arduino.json vendored Normal file
View File

@ -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"
}

View File

@ -0,0 +1,105 @@
// Example sketch which shows how to display some patterns
// on a 64x32 LED matrix
//
#include <ESP32-HUB75-MatrixPanel-I2S-DMA.h>
#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; w<strlen(firstLine); w++) {
dma_display->setTextColor(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);
}

22
src/place/manage.py Executable file
View File

@ -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()

View File

16
src/place/place/asgi.py Normal file
View File

@ -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()

124
src/place/place/settings.py Normal file
View File

@ -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'

25
src/place/place/urls.py Normal file
View File

@ -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/<int:x>/<int:y>/<str:color>', views.update_board),
]

16
src/place/place/wsgi.py Normal file
View File

@ -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()

View File

View File

@ -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)

View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class WebsiteConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'website'

View File

View File

@ -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}"

View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

155
src/place/website/views.py Normal file
View File

@ -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 = """
<style>
table { border-collapse: collapse; width: 100%; height: 90vh; border: 1px solid white;}
td { width: 10px; height: 10px; border: 1px solid gray; }
td:onclick { border: 2px solid white; }
td:hover { border: 2px solid white; }
.color {
display: flex;
width: 100%;
justify-content: space-between;
}
</style>
"""
selected_pixel = """
<script>
async function selected_pixel(element) {
var color = document.getElementById("color_picker").value;
var arr = element.id.split("x")[1].split("y");
var x = arr[0];
var y = arr[1];
var url = "http://127.0.0.1:8000/board/" + x + "/" + y + "/" + color.replace("#", "%23");
let response = await fetch(url);
let result = await response.text();
console.log(result);
if (result != "RESTRICTED") {
element.style.backgroundColor = color;
}
else {
alert("You have placed your pixel. Please wait.");
}
}
</script>
"""
html = f"<html><head><title>Ballin</title></head>{style}<body>{selected_pixel}<table><form>"
for row in board:
html += "<tr>"
x = 0
for col in board[row]:
html += f"<td style='background-color:{col};' id={'x'+ str(x) + 'y'+ str(row)} onclick=selected_pixel(this)></td>"
x += 1
html += "</tr>"
html += "</table><div class='color'><input type='color' id='color_picker'><p color='white' ><------ pick ur color</p></div></body></html>"
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)