Old SandTable project

This commit is contained in:
Your Name
2026-08-08 16:11:57 +03:00
commit 59c9f942f0
68 changed files with 2686 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
default_app_config = 'sandtable.apps.SandtableConfig'
+3
View File
@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.
+10
View File
@@ -0,0 +1,10 @@
from django.apps import AppConfig
class SandtableConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'sandtable'
def ready(self):
# Startup code here
pass
+14
View File
@@ -0,0 +1,14 @@
from django import forms
from .models import *
class UploadFileForm(forms.ModelForm):
class Meta:
model = Tracks
fields = ('file', )
class AddToQueueForm(forms.ModelForm):
class Meta:
model = Queue
fields = ()
+34
View File
@@ -0,0 +1,34 @@
# Generated by Django 4.0 on 2022-01-23 13:37
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Queue',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('file', models.CharField(max_length=200)),
('track_length', models.IntegerField()),
('pic', models.CharField(max_length=200)),
],
),
migrations.CreateModel(
name='Tracks',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('file', models.FileField(upload_to='tracks/')),
('name', models.CharField(max_length=200)),
('pic', models.ImageField(upload_to='')),
('track_length', models.IntegerField(default=0)),
('uploaded_at', models.DateTimeField(auto_now_add=True)),
],
),
]
+25
View File
@@ -0,0 +1,25 @@
# Generated by Django 4.0 on 2022-01-30 09:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('sandtable', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='RGBW',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('r', models.IntegerField(default=0)),
('g', models.IntegerField(default=0)),
('b', models.IntegerField(default=0)),
('w', models.IntegerField(default=0)),
('led_track', models.CharField(default='None', max_length=100)),
('changed', models.BooleanField(default=False)),
],
),
]
@@ -0,0 +1,25 @@
# Generated by Django 4.0 on 2022-02-13 14:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('sandtable', '0002_rgbw'),
]
operations = [
migrations.CreateModel(
name='Settings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('motor_speed', models.IntegerField(default=0)),
],
),
migrations.AddField(
model_name='rgbw',
name='led_speed',
field=models.IntegerField(default=100),
),
]
@@ -0,0 +1,33 @@
# Generated by Django 4.0 on 2022-10-14 20:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('sandtable', '0003_settings_rgbw_led_speed'),
]
operations = [
migrations.AddField(
model_name='rgbw',
name='led_intensity',
field=models.IntegerField(default=100),
),
migrations.AddField(
model_name='rgbw',
name='led_saturation',
field=models.IntegerField(default=0),
),
migrations.AlterField(
model_name='rgbw',
name='led_speed',
field=models.IntegerField(default=1),
),
migrations.AlterField(
model_name='settings',
name='motor_speed',
field=models.IntegerField(default=1),
),
]
@@ -0,0 +1,23 @@
# Generated by Django 4.0 on 2022-10-16 11:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('sandtable', '0004_rgbw_led_intensity_rgbw_led_saturation_and_more'),
]
operations = [
migrations.AddField(
model_name='settings',
name='r',
field=models.FloatField(default=0.0),
),
migrations.AddField(
model_name='settings',
name='theta',
field=models.FloatField(default=0.0),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 4.1.3 on 2022-12-01 21:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('sandtable', '0005_settings_r_settings_theta'),
]
operations = [
migrations.AddField(
model_name='settings',
name='calibrate',
field=models.IntegerField(default=0),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 4.2.5 on 2023-09-06 19:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('sandtable', '0006_settings_calibrate'),
]
operations = [
migrations.AlterField(
model_name='rgbw',
name='led_track',
field=models.IntegerField(default=0),
),
]
View File
+38
View File
@@ -0,0 +1,38 @@
from django.db import models
# Creating track entries
class Tracks(models.Model):
file = models.FileField(upload_to='tracks/')
name = models.CharField(max_length=200)
pic = models.ImageField()
track_length = models.IntegerField(default=0)
uploaded_at = models.DateTimeField(auto_now_add=True)
# Creating entries for the queue
class Queue(models.Model):
file = models.CharField(max_length=200)
track_length = models.IntegerField()
pic = models.CharField(max_length=200)
# Putting LED strip values to database
class RGBW(models.Model):
r = models.IntegerField(default=0)
g = models.IntegerField(default=0)
b = models.IntegerField(default=0)
w = models.IntegerField(default=0)
led_track = models.IntegerField(default=0)
changed = models.BooleanField(default=False)
led_speed = models.IntegerField(default=1)
led_intensity = models.IntegerField(default=100)
led_saturation = models.IntegerField(default=0)
# Settings
class Settings(models.Model):
motor_speed = models.IntegerField(default=1)
r = models.FloatField(default=0.0)
theta = models.FloatField(default=0.0)
calibrate = models.IntegerField(default=0)
+323
View File
@@ -0,0 +1,323 @@
:root {
font-size: 16px;
font-family: 'Open Sans';
--text-primary: #b6b6b6;
--text-secondary: #ececec;
--bg-primary: #121212;
--bg-secondary: #222222;
}
body {
background-color: var(--bg-primary);
margin: 0;
padding: 0;
height: 100vh;
}
.header{
position: fixed;
width: 100vw;
top: 0;
height: 8rem;
background-color: var(--bg-secondary);
}
.center {
text-align: center;
color: var(--text-secondary);
display: flex;
flex-direction: column;
justify-content: center;
}
.center a {
font-size: 50px;
}
/* Navigation bar */
.nav {
width: 100%;
height: 8rem;
position: fixed;
bottom: 0;
background-color: var(--bg-secondary);
}
ul.nav-list {
text-align: center;
display: flex;
justify-content: center;
margin-left: -50px;
margin-top: 0px;
height: 95%;
list-style-type: none;
align-items: center;
}
ul.nav-list li {
width: 25%;
padding-top: 1rem;
}
ul.nav-list .icon {
font-size: 70px;
color: var(--text-primary);
}
/* Index page */
main.index {
display: flex;
flex-direction: column;
justify-content: space-between;
height: 85%;
}
#canvas-div {
visibility: visible;
height: 0;
padding-bottom: 100%;
width: 90%;
max-width: 1000px;
margin: auto;
margin-bottom: 0;
margin-top: 8rem;
}
.track-playing {
font-size: 40px;
text-align: center;
color: var(--text-primary);
}
.track-playing p {
margin: 0;
}
.media {
display: flex;
justify-content: space-evenly;
text-align: center;
align-items: center;
}
.media a {
width: 33%;
color: var(--text-primary);
}
.media .icon {
font-size: 70px;
}
.media #play .icon {
font-size: 200px;
}
.pause {
display: none;
}
/* Tracks page */
main.tracks {
height: 100%;
}
.header-list {
height: 100%;
display: flex;
justify-content: space-evenly;
align-items: center;
list-style-type: none;
margin: 0;
padding: 0;
}
.header-list li {
text-align: center;
}
.header-list a {
color: var(--text-secondary);
text-decoration: none;
font-size: 50px;
}
.track-list-div {
padding-top: 8rem;
padding-bottom: 12rem;
}
ul.track-list {
text-align: center;
display: flex;
justify-content: flex-start;
flex-wrap: wrap;
margin-top: 0px;
list-style-type: none;
color: var(--text-primary);
}
ul.track-list li {
display: flex;
flex-direction: column;
justify-content: flex-start;
width: 50%;
}
ul.track-list a {
margin-left: 15%;
margin-right: 15%;
}
ul.track-list .list-pic {
align-self: center;
width: 100%;
}
ul.track-list .track-name {
color: var(--text-primary);
text-decoration: none;
font-size: 30px;
}
.upload-div {
position: fixed;
bottom: 10rem;
width: 100vw;
text-align: center;
}
.upload-div a {
color: var(--text-secondary);
background-color: var(--bg-primary);
text-decoration: none;
font-size: 30px;
}
.empty {
text-align: center;
font-size: 20px;
}
/* Upload page */
main.upload {
color: var(--text-primary);
text-decoration: none;
}
/* Queue page */
main.queue {
height: 85%;
}
.queue-list-div {
height: 75vh;
}
.queue-list {
display: flex;
flex-direction: column;
justify-content: flex-start;
list-style-type: decimal;
color: var(--text-primary);
font-size: 20px;
}
ul.queue-list li {
display: flex;
flex-direction: column;
justify-content: flex-start;
width: 50%;
}
.queue-list a {
margin-left: 30px;
color: var(--text-primary);
text-decoration: none;
font-size: 30px;
}
/* Track page */
.track-info {
margin-top: 8rem;
width: 98vw;
}
.track-info img {
width: 100%;
}
/* Color-picker div */
#picker {
width: 100%;
max-width: 1000px;
margin: auto;
margin-bottom: 0;
margin-top: 0;
position: absolute;
}
#picker .IroColorPicker {
margin: 20px;
margin-bottom: 20px;
}
div#rgbw {
height: 100vw;
}
div#rgbw ul {
padding-inline-start: 0;
height: 95%;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
list-style-type: none;
align-content: space-between;
}
div#rgbw li {
text-align: start;
margin: 0;
width: 40%;
}
div#rgbw li.right {
text-align: end;
padding-inline-end: 5px;
}
.dot {
height: 5rem;
width: 5rem;
margin-left: 0;
margin-right: 0;
border-radius: 50%;
display: inline-block;
font-size: 20px;
color: var(--text-primary);
text-align: center;
}
.dot p {
font-size: 35px;
margin-top: 0px;
}
.dot.red {
background-color: red;
}
.dot.green {
background-color: green;
}
.dot.blue {
background-color: blue;
}
.dot.white {
background-color: white;
}
/* Settings page */
.settings-list {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
list-style-type: none;
}
.settings-list li {
margin: 40px 0;
text-align: center;
}
.btn {
background-color: var(--bg-primary);
color: var(--text-secondary);
border-radius: 50%;
font-size: 40px;
padding: 20px 50px;
}
.settings-list a {
color: var(--text-secondary);
font-size: 30px;
}
#speedSlider {
width: 80vw;
}
#ledSlider {
width: 80vw;
}
#ledIntensitySlider {
width: 80vw;
}
#ledSaturationSlider {
width: 80vw;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

+35
View File
@@ -0,0 +1,35 @@
var body = document.body;
body.style.margin = 0;
var div = document.getElementById('canvas-div');
var canvas = document.getElementById('chart');
canvas.height = div.offsetHeight;
canvas.width = div.offsetWidth;
var x_center = div.offsetWidth/2
var y_center = div.offsetWidth/2
var ctx = canvas.getContext("2d");
// sand background
var sand = new Image();
sand.src = "static/images/sand.jpg"
var pat = ctx.createPattern(sand, "repeat")
ctx.save();
ctx.beginPath();
ctx.fillStyle = pat;
ctx.arc(x_center, y_center, div.offsetWidth/2-5, 0, 2 * Math.PI);
ctx.fill();
ctx.strokeStyle = 'white';
ctx.lineWidth = 5;
ctx.stroke();
// sand leds
var leds = ctx.createRadialGradient(x_center, y_center, 0, x_center, y_center, div.offsetWidth/2+50)
leds.addColorStop(0, "rgba(255,0,255,0.1)")
leds.addColorStop(1, "rgba(255,0,255,0.3)")
ctx.fillStyle = leds;
ctx.fill();
+94
View File
@@ -0,0 +1,94 @@
var div = document.getElementById('picker')
var red = document.getElementById('red')
var green = document.getElementById('green')
var blue = document.getElementById('blue')
var white = document.getElementById('white')
var red_value = red.innerHTML
var green_value = green.innerHTML
var blue_value = blue.innerHTML
var white_value = white.innerHTML
var colorPicker = new iro.ColorPicker('#picker', {
// Set the size of the color picker
width: div.offsetWidth-40,
// Set the initial color to pure red
color: {r: red_value, g: green_value, b: blue_value},
sliderSize: 100,
handleRadius: 35,
margin: 20,
wheelLightness: false
});
var whitePicker = new iro.ColorPicker('#picker', {
width: div.offsetWidth-40,
color: {r: white_value, g: white_value, b: white_value},
sliderSize: 100,
handleRadius: 35,
value: 0,
layout: [
{
component: iro.ui.Slider,
options: {
sliderType: 'value'
}
},
]
});
colorPicker.on('input:end', function(color) {
// Get RGB values
var R = color.rgb['r'];
var G = color.rgb['g'];
var B = color.rgb['b'];
// Write value to div
red.innerHTML = R;
green.innerHTML = G;
blue.innerHTML = B;
// POST values without refreshing
const formData = new FormData();
formData.append('r', R);
formData.append('g', G);
formData.append('b', B);
formData.append('csrfmiddlewaretoken', '{{ csrf_token }}');
console.log(formData);
fetch('/color/', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
console.log('Success:', data);
})
.catch(error => {
console.error('Error:', error);
});
});
whitePicker.on('input:end', function(color) {
// Get white value
var W = parseInt(color.value * 2.555);
// Write value to div
white.innerHTML = W;
// POST values without refreshing
const formData = new FormData();
formData.append('w', W);
formData.append('csrfmiddlewaretoken', '{{ csrf_token }}');
console.log(formData);
fetch('/color/', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
console.log('Success:', data);
})
.catch(error => {
console.error('Error:', error);
});
});
File diff suppressed because one or more lines are too long
+47
View File
@@ -0,0 +1,47 @@
{% load static %}
<!doctype html>
<html>
<head>
<link rel="stylesheet" type="text/css" href ="{% static 'css/style.css' %}">
</head>
<body>
{% block content %}{% endblock content %}
<nav class="nav">
<ul class="nav-list">
<li>
<a href="{% url 'sandtable:index' %}">
<span class="icon"><ion-icon name="infinite-outline"></ion-icon></span>
</a>
</li>
<li>
<a href="{% url 'sandtable:tracks' %}">
<span class="icon"><ion-icon name="list-outline"></ion-icon></span>
</a>
</li>
<li>
<a href="{% url 'sandtable:color' %}">
<span class="icon"><ion-icon name="color-filter-outline"></ion-icon></span>
</a>
</li>
<li>
<a href="{% url 'sandtable:settings' %}">
<span class="icon"><ion-icon name="settings-outline"></ion-icon></span>
</a>
</li>
</ul>
</nav>
<script type="module" src="https://unpkg.com/ionicons@5.5.2/dist/ionicons/ionicons.esm.js"></script>
<script nomodule src="https://unpkg.com/ionicons@5.5.2/dist/ionicons/ionicons.js"></script>
</body>
</html>
@@ -0,0 +1,31 @@
{% extends "sandtable/base.html" %}
{% load static %}
{% block content %}
<main>
<div class="track-list-div">
<ul class="settings-list">
<li>
<form action="#" method="get">
<input type="submit" class="btn" value="+1" name="+1">
</form>
</li>
<li>
<form action="#" method="get">
<input type="submit" class="btn" value="-1" name="-1">
</form>
</li>
<li>
<form action="#" method="get">
<input type="submit" class="btn" value="+10" name="+10">
</form>
</li>
<li>
<form action="#" method="get">
<input type="submit" class="btn" value="-10" name="-10">
</form>
</li>
</ul>
</div>
</main>
{% endblock content %}
@@ -0,0 +1,47 @@
{% extends "sandtable/base.html" %}
{% load static %}
{% block content %}
<main class='colorPicker'>
<div class="header center">
<a>Color picker</a>
</div>
<div class="track-list-div color-div">
<div id="picker"></div>
<div id="rgbw">
<ul>
<li>
<a>
<span class="dot red">R
<p id="red">{{ colors.r }}</p>
</span>
</a>
</li>
<li class="right">
<a>
<span class="dot green">G
<p id="green">{{ colors.g }}</p>
</span>
</a>
</li>
<li>
<a>
<span class="dot blue">B
<p id="blue">{{ colors.b }}</p>
</span>
</a>
</li>
<li class="right">
<a>
<span class="dot white">W
<p id="white">{{ colors.w }}</p>
</span>
</a>
</li>
</ul>
</div>
</div>
</main>
<script src="{% static 'js/iro.min.js' %}"></script>
<script src="{% static 'js/color.js' %}"></script>
{% endblock content %}
+58
View File
@@ -0,0 +1,58 @@
{% extends "sandtable/base.html" %}
{% load static %}
{% block content %}
<main class='index'>
<div id="canvas-div">
<canvas id="chart"></canvas>
<script src="{% static 'js/chart.js' %}"></script>
</div>
<div class='track-playing'>
<p>{{ track_name }}</p>
</div>
<div class='media'>
<form action="#" method="get" id="eraseForm">
<a id='erase'>
<input type="hidden" name="eraseButton" value="true">
<span class="icon"><ion-icon name="trash-outline"></ion-icon></span>
</a>
</form>
<form action="#" method="get" id="playForm">
<input type="hidden" name="playButton" value="true">
<a id='play'>
{% if playing %}
<span class="icon"><ion-icon name="pause-outline"></ion-icon></span>
{% else %}
<span class="icon"><ion-icon name="play-outline"></ion-icon></span>
{% endif %}
</a>
</form>
<form action="#" method="get" id="nextForm">
<input type="hidden" name="nextButton" value="true">
<a id='next' href="#">
<span class="icon"><ion-icon name="play-forward-outline"></ion-icon></span>
</a>
</form>
</div>
<script>
var play_button = document.getElementById("play");
var erase_button = document.getElementById("erase");
var next_button = document.getElementById("next");
play_button.onpointerup = function() {
document.getElementById("playForm").submit();
}
erase_button.onpointerup = function() {
document.getElementById("eraseForm").submit();
}
next_button.onpointerup = function() {
document.getElementById("nextForm").submit();
}
</script>
</main>
{% endblock content %}
+33
View File
@@ -0,0 +1,33 @@
{% extends "sandtable/base.html" %}
{% load static %}
{% block content %}
<main class='queue'>
<div class="header">
<ul class="header-list">
<li>
<a href="{% url 'sandtable:tracks' %}">Tracks</a>
</li>
<li>
<a href="{% url 'sandtable:queue' %}">Queue</a>
</li>
</ul>
</div>
<div class="track-list-div">
<ul class="queue-list">
{% for track in queue %}
<li>
<a>{{ track.file }}</a>
</li>
{% empty %}
<div class='empty'><a>Queue is empty.</a></div>
{% endfor %}
</ul>
</div>
<div class="upload-div">
<form action="#" method="get">
<input type="submit" class="btn" value="Clear queue" name="clearQueue">
</form>
</div>
</main>
{% endblock content %}
+128
View File
@@ -0,0 +1,128 @@
{% extends "sandtable/base.html" %}
{% load static %}
{% block content %}
<main class='settings'>
<div class="header center">
<a>Settings</a>
</div>
<div class="track-list-div">
<ul class="settings-list">
<li>
<form action="#" method="get">
<input type="submit" class="btn" value="Led fade" name="fadeButton">
</form>
</li>
<li>
<form action="#" method="get">
<input type="submit" class="btn" value="Home" name="homeButton">
</form>
</li>
<li>
<form action="#" method="get">
<input type="submit" class="btn" value="Calibrate" name="calibrateButton">
</form>
</li>
<li>
<form action="#" method="get">
<input type="submit" class="btn" value="Update coords" name="updateButton">
</form>
</li>
<li>
<form action="#" method="get">
<input type="submit" class="btn" value="Stop motors" name="stopButton">
</form>
</li>
<li>
<form action="#" method="get">
<input type="submit" class="btn" value="Power off" name="powerButton">
</form>
</li>
<li>
<form action="#" method="get">
<input type="submit" class="btn" value="Reset core" name="resetButton">
</form>
</li>
<li>
<a>Motor speed:</a>
<form action="#" method="get" id="motorSliderForm">
<input type="range" id="speedSlider" min="1" max="10" value={{ motorSpeed }} name="motorSpeedSlider">
<a id="speedSliderValue"></a>
</form>
</li>
<li>
<a>Led speed:</a>
<form action="#" method="get" id="ledSliderForm">
<input type="range" id="ledSlider" min="1" max="200" value={{ ledSpeed }} name="ledSpeedSlider">
<a id="ledSliderValue"></a>
</form>
</li>
<li>
<a>Led intensity:</a>
<form action="#" method="get" id="ledIntensitySliderForm">
<input type="range" id="ledIntensitySlider" min="0" max="100" value={{ ledIntensity }} name="ledIntensitySlider">
<a id="ledIntensitySliderValue"></a>
</form>
</li>
<li>
<a>Led saturation:</a>
<form action="#" method="get" id="ledSaturationSliderForm">
<input type="range" id="ledSaturationSlider" min="0" max="100" value={{ ledSaturation }} name="ledSaturationSlider">
<a id="ledSaturationSliderValue"></a>
</form>
</li>
</ul>
</div>
<script>
var speedSlider = document.getElementById("speedSlider");
var speedValue = document.getElementById("speedSliderValue");
speedValue.innerHTML = speedSlider.value;
speedSlider.oninput = function() {
speedValue.innerHTML = this.value;
}
speedSlider.onpointerup = function() {
document.getElementById("motorSliderForm").submit();
}
var ledSlider = document.getElementById("ledSlider");
var ledValue = document.getElementById("ledSliderValue");
ledValue.innerHTML = ledSlider.value;
ledSlider.oninput = function() {
ledValue.innerHTML = this.value;
}
ledSlider.onpointerup = function() {
document.getElementById("ledSliderForm").submit();
}
var ledIntensitySlider = document.getElementById("ledIntensitySlider");
var ledIntensityValue = document.getElementById("ledIntensitySliderValue");
ledIntensityValue.innerHTML = ledIntensitySlider.value;
ledIntensitySlider.oninput = function() {
ledIntensityValue.innerHTML = this.value;
}
ledIntensitySlider.onpointerup = function() {
document.getElementById("ledIntensitySliderForm").submit();
}
var ledSaturationSlider = document.getElementById("ledSaturationSlider");
var ledSaturationValue = document.getElementById("ledSaturationSliderValue");
ledSaturationValue.innerHTML = ledSaturationSlider.value;
ledSaturationSlider.oninput = function() {
ledSaturationValue.innerHTML = this.value;
}
ledSaturationSlider.onpointerup = function() {
document.getElementById("ledSaturationSliderForm").submit();
}
</script>
</main>
{% endblock content %}
+24
View File
@@ -0,0 +1,24 @@
{% extends "sandtable/base.html" %}
{% load static %}
{% block content %}
<main>
<div class="header center">
<a>{{ track.name }}</a>
<a style="font-size: 20px;">Length: {{ track.track_length }}</a>
</div>
<div class="track-info">
<form action="#" method="get">
<input type="submit" class="btn" value="Delete track" name="deleteTrack">
</form>
<img src="{{ track.pic.url }}">
</div>
<div class="center">
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.field.as_hidden }}
<button class="btn" type="submit">Add to queue</button>
</form>
</div>
</main>
{% endblock content %}
+34
View File
@@ -0,0 +1,34 @@
{% extends "sandtable/base.html" %}
{% load static %}
{% block content %}
<main class='tracks'>
<div class="header">
<ul class="header-list">
<li>
<a href="{% url 'sandtable:tracks' %}">Tracks</a>
</li>
<li>
<a href="{% url 'sandtable:queue' %}">Queue</a>
</li>
</ul>
</div>
<div class="track-list-div">
<ul class="track-list">
{% for track in tracks %}
<li>
<a href="{% url 'sandtable:track' track.id %}">
<img class="list-pic" src="{{ track.pic.url }}">
<a class="track-name" href="{% url 'sandtable:track' track.id %}">{{ track.name }}</a>
</a>
</li>
{% empty %}
<div class='empty'><a>No tracks have been added yet.</a></div>
{% endfor %}
</ul>
</div>
<div class="upload-div">
<a href="{% url 'sandtable:upload' %}">Upload</a>
</div>
</main>
{% endblock content %}
@@ -0,0 +1,14 @@
{% extends "sandtable/base.html" %}
{% load static %}
{% block content %}
<main class='upload'>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Upload</button>
</form>
<p><a href="{% url 'sandtable:tracks' %}">Return to home</a></p>
</main>
{% endblock content %}
+3
View File
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
+16
View File
@@ -0,0 +1,16 @@
from unicodedata import name
from django.urls import path, include
from .views import *
from .forms import *
app_name = 'sandtable'
urlpatterns = [
path('', index, name='index'),
path('tracks/', tracks, name='tracks'),
path('queue/', queue, name='queue'),
path('settings/', settings, name='settings'),
path('calibrate/', calibrate, name='calibrate'),
path(r'color/', color, name='color'),
path('upload/', uploadTracks, name='upload'),
path('tracks/<int:track_id>', track, name='track')
]
+436
View File
@@ -0,0 +1,436 @@
import colorsys
from xml.dom import ValidationErr
from django.shortcuts import render, redirect
from .models import *
from .forms import *
import thr2png, os, psutil, writeSerial, playControl
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
# -----------------------------------------------------------------------------
# LED handling
#
# The device no longer stores an LED track itself - every speed/intensity/
# saturation/color change has to rebuild the full track and resend it via
# writeSerial.setLedTrack(). What mode is "currently active" is tracked in
# the RGBW row (id=1) itself via led_track:
#
# LED_TRACK_SOLID (0) - use the stored r/g/b/w directly, as a one-color track
# LED_TRACK_FADE (5) - cycle through FADE_COLORS (matches what fadeButton
# used to trigger on the old firmware)
#
# I only wired up these two because they're the only two entry points that
# exist in this file (color() and the fadeButton branch below). If there
# were other led_track presets (1-4) driving different sequences on the old
# firmware, tell me what they were and I'll add them.
# -----------------------------------------------------------------------------
LED_TRACK_SOLID = 0
LED_TRACK_FADE = 5
FADE_COLORS = [
(255, 0, 0, 0), # Red
(255, 255, 0, 0), # Yellow
(0, 255, 0, 0), # Green
(0, 255, 255, 0), # Cyan
(0, 0, 255, 0), # Blue
]
def _scale_color(color, intensity_pct, saturation_pct=None):
"""Apply intensity (0-100, brightness) and, for hued preset colors,
saturation (0-100) to an (r, g, b, w) tuple. The white channel only
gets intensity applied - there's no meaningful "saturation" of white."""
# Defensive: Django doesn't coerce a model field's type on a plain
# attribute assignment (only on a DB round trip), so values coming
# straight from request.POST/GET can still be strings here - cast
# explicitly rather than letting the arithmetic below blow up. Also
# guard against None, since the color-picker only ever sets r/g/b OR w
# at once, so the other channel(s) can be unset on a fresh row.
r, g, b, w = (int(c) if c is not None else 0 for c in color)
if saturation_pct is not None:
h, s, v = colorsys.rgb_to_hsv(r / 255, g / 255, b / 255)
s *= max(0.0, min(saturation_pct, 100.0)) / 100.0
r, g, b = colorsys.hsv_to_rgb(h, s, v)
r, g, b = r * 255, g * 255, b * 255
scale = max(0.0, min(intensity_pct, 100.0)) / 100.0
return (
round(r * scale),
round(g * scale),
round(b * scale),
round(w * scale),
)
def _sendCurrentLed(led_data):
"""Rebuild whatever LED mode is currently active from its stored
settings and resend the full track."""
speed = int(led_data.led_speed or writeSerial.MIN_TRACK_SPEED)
intensity = float(led_data.led_intensity) if led_data.led_intensity is not None else 100.0
saturation = float(led_data.led_saturation) if led_data.led_saturation is not None else 100.0
if led_data.led_track == LED_TRACK_FADE:
colors = [_scale_color(c, intensity, saturation) for c in FADE_COLORS]
else:
# Solid color: saturation doesn't really mean anything for an
# explicitly hand-picked RGB value, so only intensity (brightness)
# is applied here. Speed is also irrelevant for a single color.
colors = [_scale_color((led_data.r, led_data.g, led_data.b, led_data.w), intensity)]
writeSerial.setLedTrack(colors, speed, wait_ack=False)
def index(request):
status = playControl.get_status()
playing = status['status'] in (playControl.STATUS_PLAYING, playControl.STATUS_PAUSED)
queue = Queue.objects.all()
track_name = ""
if len(queue) > 0:
track = queue[0].file
track_name = track.split('/')[-1].replace(".thr", '')
# Erase button
if(request.GET.get('eraseButton')):
playControl.stop_player()
return redirect('sandtable:index')
# Button to start playing
if(request.GET.get('playButton')):
status = playControl.get_status()
if status['status'] == playControl.STATUS_IDLE:
os.system("/bin/python /home/pi/SandTable/PlayQueue.py &")
elif status['status'] == playControl.STATUS_PLAYING:
playControl.set_command(playControl.COMMAND_PAUSE)
elif status['status'] == playControl.STATUS_PAUSED:
playControl.set_command(playControl.COMMAND_PLAY)
return redirect('sandtable:index')
# Next button
if(request.GET.get('nextButton')):
if playControl.get_status()['status'] == playControl.STATUS_IDLE:
print("Not running")
else:
playControl.set_command(playControl.COMMAND_NEXT)
print("next")
return redirect('sandtable:index')
context = { 'playing': playing, 'track_name': track_name }
return render(request, 'sandtable/index.html', context)
def tracks(request):
tracks = Tracks.objects.order_by('uploaded_at')
context = { 'tracks': tracks }
return render(request, 'sandtable/tracks.html', context)
def track(request, track_id):
track = Tracks.objects.get(id=track_id)
# Button to delete track
if(request.GET.get('deleteTrack')):
track.delete()
return redirect('sandtable:tracks')
# Add track to queue
elif request.method == 'POST':
form = AddToQueueForm(request.POST)
if form.is_valid():
new_queue_track = form.save(commit=False)
new_queue_track.file = track.file
new_queue_track.track_length = track.track_length
new_queue_track.pic = track.pic
new_queue_track.save()
# Start playing
if playControl.get_status()['status'] == playControl.STATUS_IDLE:
os.system("/bin/python /home/pi/SandTable/PlayQueue.py &")
return redirect('sandtable:tracks')
else:
form = UploadFileForm()
context = { 'track': track, 'form': form }
return render(request, 'sandtable/track.html', context)
def queue(request):
queue = Queue.objects.all()
context = { 'queue': queue }
# If clearQueue button is pressed then delete all entries in queue table
if(request.GET.get('clearQueue')):
Queue.objects.all().delete()
return redirect('sandtable:queue')
return render(request, 'sandtable/queue.html', context)
def uploadTracks(request):
if request.method == 'POST':
form = UploadFileForm(request.POST, request.FILES)
if form.is_valid():
new_track = form.save(commit=False)
# Get file name
file_name = request.FILES['file'].name
# If file exists then raise error
if os.path.isfile('/home/pi/SandTable/media/tracks/' + file_name):
raise ValidationErr('File already exists')
# If file is not .thr then raise error
if '.thr' not in file_name:
raise ValidationErr('Not a thr file')
name = file_name.split('.')
new_track.name = name[0]
new_track.save()
# Generate image from .thr and return track length
new_track.track_length = thr2png.drawFile('/home/pi/SandTable/media/tracks/' + file_name)
new_track.pic = 'tracks/' + name[0] + '.png'
new_track.save()
return redirect('sandtable:tracks')
else:
form = UploadFileForm()
context = { 'form': form }
return render(request, 'sandtable/uploadTracks.html', context)
def settings(request):
settings_data = Settings.objects.get(id=0)
motor_speed = settings_data.motor_speed
led_data = RGBW.objects.get(id=1)
led_speed = led_data.led_speed
led_intensity = led_data.led_intensity
led_saturation = led_data.led_saturation
# Button to home motors
if(request.GET.get('homeButton')):
playControl.stop_player()
writeSerial.home(wait_ack=False)
# No more r/theta reset here - the firmware tracks position itself
# now, and calibration offsets are stored firmware-side too (see
# COMMAND_SET_OFFSET), so there's nothing left to reapply after a
# home like the old code used to do with COMMAND_M2_STEP.
return redirect('sandtable:settings')
# Button to calibrate
if(request.GET.get('calibrateButton')):
return redirect('sandtable:calibrate')
# Button to stop motors
elif(request.GET.get('stopButton')):
playControl.stop_player()
# ASSUMPTION: mapped the old immediate-stop button to
# COMMAND_DISABLE_MOTORS since there's no direct "stop" in the new
# enum. This likely de-energizes the steppers rather than just
# pausing them, so you'll probably need to re-home before playing
# again - confirm this is the behavior you want.
writeSerial.disableMotors(wait_ack=False)
return redirect('sandtable:settings')
# Button to power down the pi
elif(request.GET.get('powerButton')):
playControl.stop_player()
os.system("sudo shutdown -h")
return redirect('sandtable:settings')
# Button to reset core 1
elif(request.GET.get('resetButton')):
# TODO: no equivalent to the old COMMAND_RESET in the new
# commands_e enum. If resetting core 1 is still needed, this needs
# a new firmware command.
print("resetButton: not implemented under the new protocol yet")
return redirect('sandtable:settings')
# Button to do led fade
elif(request.GET.get('fadeButton')):
led_data.led_track = LED_TRACK_FADE
led_data.save()
_sendCurrentLed(led_data)
return redirect('sandtable:settings')
# Motor speed slider
elif(request.GET.get('motorSpeedSlider')):
value = request.GET.get('motorSpeedSlider')
settings_data.motor_speed = int(value)
settings_data.save()
writeSerial.setMotorSpeed(int(value), wait_ack=False)
return redirect('sandtable:settings')
# Led speed slider
elif(request.GET.get('ledSpeedSlider')):
value = request.GET.get('ledSpeedSlider')
led_data.led_speed = int(value)
led_data.save()
_sendCurrentLed(led_data)
return redirect('sandtable:settings')
# Led intensity slider
elif(request.GET.get('ledIntensitySlider')):
value = request.GET.get('ledIntensitySlider')
led_data.led_intensity = int(value)
led_data.save()
_sendCurrentLed(led_data)
return redirect('sandtable:settings')
# Led saturation slider
elif(request.GET.get('ledSaturationSlider')):
value = request.GET.get('ledSaturationSlider')
led_data.led_saturation = int(value)
led_data.save()
_sendCurrentLed(led_data)
return redirect('sandtable:settings')
context = { 'motorSpeed': motor_speed, 'ledSpeed': led_speed, 'ledIntensity': led_intensity, 'ledSaturation': led_saturation }
return render(request, 'sandtable/settings.html', context)
def calibrate(request):
settings_data = Settings.objects.get(id=0)
# Calibration is now handled firmware-side (COMMAND_SET_OFFSET adds to
# a stored per-motor step offset, COMMAND_RESET_OFFSET zeros it out).
# These buttons only ever adjusted motor 2 in the old code (M2_STEP),
# so that's preserved here - m1's delta is always 0. `settings_data.
# calibrate` is kept purely as a local display counter mirroring what's
# been sent; the firmware is the actual source of truth for the offset
# itself.
if(request.GET.get('+1')):
writeSerial.setOffset(0, 1, wait_ack=False)
settings_data.calibrate += 1
settings_data.save()
return redirect('sandtable:calibrate')
if(request.GET.get('-1')):
writeSerial.setOffset(0, -1, wait_ack=False)
settings_data.calibrate -= 1
settings_data.save()
return redirect('sandtable:calibrate')
if(request.GET.get('+10')):
writeSerial.setOffset(0, 10, wait_ack=False)
settings_data.calibrate += 10
settings_data.save()
return redirect('sandtable:calibrate')
if(request.GET.get('-10')):
writeSerial.setOffset(0, -10, wait_ack=False)
settings_data.calibrate -= 10
settings_data.save()
return redirect('sandtable:calibrate')
# New: full reset of the firmware's calibration offset. There wasn't a
# button for this before (COMMAND_RESET_OFFSET didn't exist yet) - I've
# added the handler so it's ready, but you'll need to add the actual
# button/link in calibrate.html to trigger it (?resetButton=1).
if(request.GET.get('resetButton')):
writeSerial.resetOffset(wait_ack=False)
settings_data.calibrate = 0
settings_data.save()
return redirect('sandtable:calibrate')
return render(request, 'sandtable/calibrate.html')
@csrf_exempt
def color(request):
# If entry in the table does not exist, then create new with id=1
if not RGBW.objects.filter(id=1).exists():
if request.method == 'POST':
newcolor = RGBW()
newcolor.id = 1
# If POST contains RGB or WHITE
if 'r' in request.POST:
newcolor.r = int(request.POST.get('r'))
newcolor.g = int(request.POST.get('g'))
newcolor.b = int(request.POST.get('b'))
elif 'w' in request.POST:
newcolor.w = int(request.POST.get('w'))
# Don't know why I thought that I needed this
newcolor.changed = True
newcolor.led_track = LED_TRACK_SOLID
newcolor.save()
_sendCurrentLed(newcolor)
return HttpResponse('')
else:
context = { 'colors': {'r':0, 'g':0, 'b':0, 'w':0} }
return render(request, 'sandtable/colorPicker.html', context)
else:
colors = RGBW.objects.get(id=1)
if request.method == 'POST':
# If POST contains RGB or WHITE
if 'r' in request.POST:
colors.r = int(request.POST.get('r'))
colors.g = int(request.POST.get('g'))
colors.b = int(request.POST.get('b'))
elif 'w' in request.POST:
colors.w = int(request.POST.get('w'))
# Don't know why I thought that I needed this
colors.changed = True
colors.led_track = LED_TRACK_SOLID
colors.save()
_sendCurrentLed(colors)
return HttpResponse('')
else:
context = { 'colors': colors }
return render(request, 'sandtable/colorPicker.html', context)
# Find if process is running and killit if needed
# NOTE: no longer used by the play/pause/stop/skip buttons above (they use
# playControl instead) - left in place in case anything else in the project
# still calls it.
def checkProcesses(name, kill=False):
for proc in psutil.process_iter(attrs=["pid", "name", "cmdline"]):
try:
# Check if process name contains the given name string.
if "python" in proc.name():
for cmd in proc.cmdline():
if name.lower() in cmd.lower():
# Kill the process if kill=True
if kill:
proc.kill()
return proc.pid
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
pass
return False