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