Old SandTable project
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user